Initial commit
This commit is contained in:
commit
3845538d92
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
||||
14
Cargo.toml
Normal file
14
Cargo.toml
Normal file
@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "voxel-engine"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bevy = { version = "0.15.1", features = ["jpeg", "trace_tracy", "trace_tracy_memory"] }
|
||||
bevy_egui = "0.31.1"
|
||||
bevy_asset = "0.15.0"
|
||||
bevy-inspector-egui = "0.28.0"
|
||||
bevy_reflect = "0.15.0"
|
||||
bevy_render = "0.15.0"
|
||||
bevy_window = "0.15.0"
|
||||
egui_dock = "0.14.0"
|
||||
BIN
assets/fonts/minecraft_font.ttf
Normal file
BIN
assets/fonts/minecraft_font.ttf
Normal file
Binary file not shown.
1
combine.sh
Normal file
1
combine.sh
Normal file
@ -0,0 +1 @@
|
||||
find src/ -type f -exec cat {} + > target/combined.txt
|
||||
82
src/app.rs
Normal file
82
src/app.rs
Normal file
@ -0,0 +1,82 @@
|
||||
use bevy::prelude::*;
|
||||
use bevy_egui::EguiSet;
|
||||
use crate::helper::debug_gizmos::debug_gizmos;
|
||||
use crate::helper::egui_dock::{reset_camera_viewport, set_camera_viewport, set_gizmo_mode, show_ui_system, UiState};
|
||||
use crate::helper::large_transform::DoubleTransform;
|
||||
|
||||
pub struct AppPlugin;
|
||||
|
||||
|
||||
#[derive(Resource, Debug)]
|
||||
pub struct InspectorVisible(pub bool);
|
||||
|
||||
impl Default for InspectorVisible {
|
||||
fn default() -> Self {
|
||||
InspectorVisible(false)
|
||||
}
|
||||
}
|
||||
impl Plugin for AppPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.insert_resource(UiState::new());
|
||||
app.insert_resource(InspectorVisible(true));
|
||||
|
||||
|
||||
|
||||
app.add_plugins(crate::plugins::camera_plugin::CameraPlugin);
|
||||
app.add_plugins(crate::plugins::ui_plugin::UiPlugin);
|
||||
|
||||
app.add_plugins(crate::plugins::environment_plugin::EnvironmentPlugin);
|
||||
app.add_plugins(crate::plugins::large_transform_plugin::LargeTransformPlugin);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
app.add_systems(Update, (debug_gizmos, toggle_ui_system));
|
||||
app.add_systems(
|
||||
PostUpdate,
|
||||
show_ui_system
|
||||
.before(EguiSet::ProcessOutput)
|
||||
.before(bevy_egui::systems::end_pass_system)
|
||||
.before(TransformSystem::TransformPropagate)
|
||||
.run_if(should_display_inspector),
|
||||
);
|
||||
app.add_systems(PostUpdate, (set_camera_viewport.after(show_ui_system).run_if(should_display_inspector), reset_camera_viewport.run_if(should_not_display_inspector).after(set_camera_viewport)));
|
||||
app.add_systems(Update, set_gizmo_mode);
|
||||
app.register_type::<Option<Handle<Image>>>();
|
||||
app.register_type::<AlphaMode>();
|
||||
app.register_type::<DoubleTransform>();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
fn toggle_ui_system(
|
||||
|
||||
keyboard_input: Res<ButtonInput<KeyCode>>,
|
||||
mut inspector_visible: ResMut<InspectorVisible>,
|
||||
){
|
||||
// =======================
|
||||
// 6) Hide Inspector
|
||||
// =======================
|
||||
if keyboard_input.just_pressed(KeyCode::F1) {
|
||||
inspector_visible.0 = !inspector_visible.0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
fn should_display_inspector(inspector_visible: Res<InspectorVisible>) -> bool {
|
||||
inspector_visible.0
|
||||
}
|
||||
|
||||
fn should_not_display_inspector(inspector_visible: Res<InspectorVisible>) -> bool {
|
||||
!inspector_visible.0
|
||||
}
|
||||
21
src/helper/debug_gizmos.rs
Normal file
21
src/helper/debug_gizmos.rs
Normal file
@ -0,0 +1,21 @@
|
||||
use bevy::color::palettes::css::{BLUE, GREEN, RED};
|
||||
use bevy::prelude::*;
|
||||
|
||||
pub fn debug_gizmos(mut gizmos: Gizmos) {
|
||||
|
||||
|
||||
/* // Draw a line
|
||||
gizmos.line(
|
||||
Vec3::ZERO,
|
||||
Vec3::new(1.0, 1.0, 1.0),
|
||||
RED,
|
||||
);
|
||||
|
||||
// Draw a sphere
|
||||
gizmos.sphere(Vec3::new(2.0, 2.0, 2.0), 0.5, BLUE);
|
||||
|
||||
// Draw a wireframe cube
|
||||
gizmos.rect(Isometry3d::IDENTITY, Vec2::ONE, GREEN);*/
|
||||
|
||||
}
|
||||
|
||||
343
src/helper/egui_dock.rs
Normal file
343
src/helper/egui_dock.rs
Normal file
@ -0,0 +1,343 @@
|
||||
use bevy::prelude::*;
|
||||
use bevy_asset::{ReflectAsset, UntypedAssetId};
|
||||
use bevy_egui::{egui, EguiContext};
|
||||
use bevy_inspector_egui::bevy_inspector::hierarchy::{hierarchy_ui, SelectedEntities};
|
||||
use bevy_inspector_egui::bevy_inspector::{
|
||||
self, ui_for_entities_shared_components, ui_for_entity_with_children,
|
||||
};
|
||||
use std::any::TypeId;
|
||||
use bevy_reflect::TypeRegistry;
|
||||
use bevy_render::camera::{CameraProjection, Viewport};
|
||||
use bevy_window::{PrimaryWindow, Window};
|
||||
use egui_dock::{DockArea, DockState, NodeIndex, Style};
|
||||
|
||||
#[cfg(egui_dock_gizmo)]
|
||||
use transform_gizmo_egui::GizmoMode;
|
||||
|
||||
/// Placeholder type if gizmo is disabled.
|
||||
#[cfg(not(egui_dock_gizmo))]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct GizmoMode;
|
||||
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct MainCamera;
|
||||
|
||||
pub fn show_ui_system(world: &mut World) {
|
||||
let Ok(egui_context) = world
|
||||
.query_filtered::<&mut EguiContext, With<PrimaryWindow>>()
|
||||
.get_single(world)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let mut egui_context = egui_context.clone();
|
||||
|
||||
world.resource_scope::<UiState, _>(|world, mut ui_state| {
|
||||
ui_state.ui(world, egui_context.get_mut())
|
||||
});
|
||||
}
|
||||
|
||||
// make camera only render to view not obpub structed by UI
|
||||
pub fn set_camera_viewport(
|
||||
ui_state: Res<UiState>,
|
||||
primary_window: Query<&mut Window, With<PrimaryWindow>>,
|
||||
egui_settings: Query<&bevy_egui::EguiSettings>,
|
||||
mut cameras: Query<&mut Camera, With<MainCamera>>,
|
||||
) {
|
||||
let mut cam = cameras.single_mut();
|
||||
|
||||
let Ok(window) = primary_window.get_single() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let scale_factor = window.scale_factor() * egui_settings.single().scale_factor;
|
||||
|
||||
let viewport_pos = ui_state.viewport_rect.left_top().to_vec2() * scale_factor;
|
||||
let viewport_size = ui_state.viewport_rect.size() * scale_factor;
|
||||
|
||||
let physical_position = UVec2::new(viewport_pos.x as u32, viewport_pos.y as u32);
|
||||
let physical_size = UVec2::new(viewport_size.x as u32, viewport_size.y as u32);
|
||||
|
||||
// The desired viewport rectangle at its offset in "physical pixel space"
|
||||
let rect = physical_position + physical_size;
|
||||
|
||||
let window_size = window.physical_size();
|
||||
// wgpu will panic if trying to set a viewport rect which has coordinates extending
|
||||
// past the size of the render target, i.e. the physical window in our case.
|
||||
// Typically this shouldn't happen- but during init and resizing etc. edge cases might occur.
|
||||
// Simply do nothing in those cases.
|
||||
if rect.x <= window_size.x && rect.y <= window_size.y {
|
||||
cam.viewport = Some(Viewport {
|
||||
physical_position,
|
||||
physical_size,
|
||||
depth: 0.0..1.0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_camera_viewport(mut cameras: Query<&mut Camera, With<MainCamera>>) {
|
||||
if let Ok(mut cam) = cameras.get_single_mut() {
|
||||
cam.viewport = None; // Reset the viewport to its default state
|
||||
}
|
||||
}
|
||||
pub fn set_gizmo_mode(input: Res<ButtonInput<KeyCode>>, mut ui_state: ResMut<UiState>) {
|
||||
#[cfg(egui_dock_gizmo)]
|
||||
let keybinds = [
|
||||
(KeyCode::KeyR, GizmoMode::Rotate),
|
||||
(KeyCode::KeyT, GizmoMode::Translate),
|
||||
(KeyCode::KeyS, GizmoMode::Scale),
|
||||
];
|
||||
#[cfg(not(egui_dock_gizmo))]
|
||||
let keybinds = [];
|
||||
for (key, mode) in keybinds {
|
||||
if input.just_pressed(key) {
|
||||
ui_state.gizmo_mode = mode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq)]
|
||||
pub enum InspectorSelection {
|
||||
Entities,
|
||||
Resource(TypeId, String),
|
||||
Asset(TypeId, String, UntypedAssetId),
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct UiState {
|
||||
state: DockState<EguiWindow>,
|
||||
viewport_rect: egui::Rect,
|
||||
selected_entities: SelectedEntities,
|
||||
selection: InspectorSelection,
|
||||
gizmo_mode: GizmoMode,
|
||||
}
|
||||
|
||||
impl UiState {
|
||||
pub fn new() -> Self {
|
||||
let mut state = DockState::new(vec![EguiWindow::GameView]);
|
||||
let tree = state.main_surface_mut();
|
||||
let [game, _inspector] =
|
||||
tree.split_right(NodeIndex::root(), 0.75, vec![EguiWindow::Inspector]);
|
||||
let [game, _hierarchy] = tree.split_left(game, 0.2, vec![EguiWindow::Hierarchy]);
|
||||
let [_game, _bottom] =
|
||||
tree.split_below(game, 0.8, vec![EguiWindow::Resources, EguiWindow::Assets]);
|
||||
|
||||
Self {
|
||||
state,
|
||||
selected_entities: SelectedEntities::default(),
|
||||
selection: InspectorSelection::Entities,
|
||||
viewport_rect: egui::Rect::NOTHING,
|
||||
#[cfg(egui_dock_gizmo)]
|
||||
gizmo_mode: GizmoMode::Translate,
|
||||
#[cfg(not(egui_dock_gizmo))]
|
||||
gizmo_mode: GizmoMode,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, world: &mut World, ctx: &mut egui::Context) {
|
||||
let mut tab_viewer = TabViewer {
|
||||
world,
|
||||
viewport_rect: &mut self.viewport_rect,
|
||||
selected_entities: &mut self.selected_entities,
|
||||
selection: &mut self.selection,
|
||||
gizmo_mode: self.gizmo_mode,
|
||||
};
|
||||
DockArea::new(&mut self.state)
|
||||
.style(Style::from_egui(ctx.style().as_ref()))
|
||||
.show(ctx, &mut tab_viewer);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum EguiWindow {
|
||||
GameView,
|
||||
Hierarchy,
|
||||
Resources,
|
||||
Assets,
|
||||
Inspector,
|
||||
}
|
||||
|
||||
pub struct TabViewer<'a> {
|
||||
world: &'a mut World,
|
||||
selected_entities: &'a mut SelectedEntities,
|
||||
selection: &'a mut InspectorSelection,
|
||||
viewport_rect: &'a mut egui::Rect,
|
||||
gizmo_mode: GizmoMode,
|
||||
}
|
||||
|
||||
impl egui_dock::TabViewer for TabViewer<'_> {
|
||||
type Tab = EguiWindow;
|
||||
|
||||
fn ui(&mut self, ui: &mut egui_dock::egui::Ui, window: &mut Self::Tab) {
|
||||
let type_registry = self.world.resource::<AppTypeRegistry>().0.clone();
|
||||
let type_registry = type_registry.read();
|
||||
|
||||
match window {
|
||||
EguiWindow::GameView => {
|
||||
*self.viewport_rect = ui.clip_rect();
|
||||
|
||||
draw_gizmo(ui, self.world, self.selected_entities, self.gizmo_mode);
|
||||
}
|
||||
EguiWindow::Hierarchy => {
|
||||
let selected = hierarchy_ui(self.world, ui, self.selected_entities);
|
||||
if selected {
|
||||
*self.selection = InspectorSelection::Entities;
|
||||
}
|
||||
}
|
||||
EguiWindow::Resources => select_resource(ui, &type_registry, self.selection),
|
||||
EguiWindow::Assets => select_asset(ui, &type_registry, self.world, self.selection),
|
||||
EguiWindow::Inspector => match *self.selection {
|
||||
InspectorSelection::Entities => match self.selected_entities.as_slice() {
|
||||
&[entity] => ui_for_entity_with_children(self.world, entity, ui),
|
||||
entities => ui_for_entities_shared_components(self.world, entities, ui),
|
||||
},
|
||||
InspectorSelection::Resource(type_id, ref name) => {
|
||||
ui.label(name);
|
||||
bevy_inspector::by_type_id::ui_for_resource(
|
||||
self.world,
|
||||
type_id,
|
||||
ui,
|
||||
name,
|
||||
&type_registry,
|
||||
)
|
||||
}
|
||||
InspectorSelection::Asset(type_id, ref name, handle) => {
|
||||
ui.label(name);
|
||||
bevy_inspector::by_type_id::ui_for_asset(
|
||||
self.world,
|
||||
type_id,
|
||||
handle,
|
||||
ui,
|
||||
&type_registry,
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn title(&mut self, window: &mut Self::Tab) -> egui_dock::egui::WidgetText {
|
||||
format!("{window:?}").into()
|
||||
}
|
||||
|
||||
fn clear_background(&self, window: &Self::Tab) -> bool {
|
||||
!matches!(window, EguiWindow::GameView)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn draw_gizmo(
|
||||
ui: &mut egui::Ui,
|
||||
world: &mut World,
|
||||
selected_entities: &SelectedEntities,
|
||||
gizmo_mode: GizmoMode,
|
||||
) {
|
||||
let (cam_transform, projection) = world
|
||||
.query_filtered::<(&GlobalTransform, &Projection), With<MainCamera>>()
|
||||
.single(world);
|
||||
let view_matrix = Mat4::from(cam_transform.affine().inverse());
|
||||
let projection_matrix = projection.get_clip_from_view();
|
||||
|
||||
if selected_entities.len() != 1 {
|
||||
#[allow(clippy::needless_return)]
|
||||
return;
|
||||
}
|
||||
|
||||
/*for selected in selected_entities.iter() {
|
||||
let Some(transform) = world.get::<Transform>(selected) else {
|
||||
continue;
|
||||
};
|
||||
let model_matrix = transform.compute_matrix();
|
||||
|
||||
let mut gizmo = Gizmo::new(GizmoConfig {
|
||||
view_matrix: view_matrix.into(),
|
||||
projection_matrix: projection_matrix.into(),
|
||||
orientation: GizmoOrientation::Local,
|
||||
modes: EnumSet::from(gizmo_mode),
|
||||
..Default::default()
|
||||
});
|
||||
let Some([result]) = gizmo
|
||||
.interact(ui, model_matrix.into())
|
||||
.map(|(_, res)| res.as_slice())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut transform = world.get_mut::<Transform>(selected).unwrap();
|
||||
transform = Transform {
|
||||
translation: Vec3::from(<[f64; 3]>::from(result.translation)),
|
||||
rotation: Quat::from_array(<[f64; 4]>::from(result.rotation)),
|
||||
scale: Vec3::from(<[f64; 3]>::from(result.scale)),
|
||||
};
|
||||
}*/
|
||||
}
|
||||
|
||||
pub fn select_resource(
|
||||
ui: &mut egui::Ui,
|
||||
type_registry: &TypeRegistry,
|
||||
selection: &mut InspectorSelection,
|
||||
) {
|
||||
let mut resources: Vec<_> = type_registry
|
||||
.iter()
|
||||
.filter(|registration| registration.data::<ReflectResource>().is_some())
|
||||
.map(|registration| {
|
||||
(
|
||||
registration.type_info().type_path_table().short_path(),
|
||||
registration.type_id(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
resources.sort_by(|(name_a, _), (name_b, _)| name_a.cmp(name_b));
|
||||
|
||||
for (resource_name, type_id) in resources {
|
||||
let selected = match *selection {
|
||||
InspectorSelection::Resource(selected, _) => selected == type_id,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if ui.selectable_label(selected, resource_name).clicked() {
|
||||
*selection = InspectorSelection::Resource(type_id, resource_name.to_string());
|
||||
}
|
||||
debug!("{}", resource_name);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_asset(
|
||||
ui: &mut egui::Ui,
|
||||
type_registry: &TypeRegistry,
|
||||
world: &World,
|
||||
selection: &mut InspectorSelection,
|
||||
) {
|
||||
let mut assets: Vec<_> = type_registry
|
||||
.iter()
|
||||
.filter_map(|registration| {
|
||||
let reflect_asset = registration.data::<ReflectAsset>()?;
|
||||
Some((
|
||||
registration.type_info().type_path_table().short_path(),
|
||||
registration.type_id(),
|
||||
reflect_asset,
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
assets.sort_by(|(name_a, ..), (name_b, ..)| name_a.cmp(name_b));
|
||||
|
||||
for (asset_name, asset_type_id, reflect_asset) in assets {
|
||||
let handles: Vec<_> = reflect_asset.ids(world).collect();
|
||||
|
||||
ui.collapsing(format!("{asset_name} ({})", handles.len()), |ui| {
|
||||
for handle in handles {
|
||||
let selected = match *selection {
|
||||
InspectorSelection::Asset(_, _, selected_id) => selected_id == handle,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if ui
|
||||
.selectable_label(selected, format!("{:?}", handle))
|
||||
.clicked()
|
||||
{
|
||||
*selection =
|
||||
InspectorSelection::Asset(asset_type_id, asset_name.to_string(), handle);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
103
src/helper/large_transform.rs
Normal file
103
src/helper/large_transform.rs
Normal file
@ -0,0 +1,103 @@
|
||||
use bevy::math::{DQuat, DVec3};
|
||||
use bevy::prelude::{Commands, Component, GlobalTransform, Query, Reflect, Res, ResMut, Resource, Transform, With, Without};
|
||||
use bevy_render::prelude::Camera;
|
||||
|
||||
|
||||
#[derive(Resource, Reflect,Default)]
|
||||
pub struct WorldOffset(pub DVec3);
|
||||
|
||||
#[derive(Component, Default,Reflect)]
|
||||
pub struct DoubleTransform {
|
||||
pub translation: DVec3,
|
||||
pub rotation: DQuat,
|
||||
pub scale: DVec3,
|
||||
}
|
||||
|
||||
impl DoubleTransform {
|
||||
pub fn new(translation: DVec3, rotation: DQuat, scale: DVec3) -> Self {
|
||||
Self {
|
||||
translation,
|
||||
rotation,
|
||||
scale,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a unit vector pointing "forward" (negative-Z) based on the rotation
|
||||
pub fn forward(&self) -> DVec3 {
|
||||
self.rotation * DVec3::new(0.0, 0.0, -1.0)
|
||||
}
|
||||
|
||||
/// Returns a unit vector pointing "right" (positive-X)
|
||||
pub fn right(&self) -> DVec3 {
|
||||
self.rotation * DVec3::new(1.0, 0.0, 0.0)
|
||||
}
|
||||
|
||||
/// Returns a unit vector pointing "up" (positive-Y)
|
||||
pub fn up(&self) -> DVec3 {
|
||||
self.rotation * DVec3::new(0.0, 1.0, 0.0)
|
||||
}
|
||||
pub fn down(&self) -> DVec3 {
|
||||
self.rotation * DVec3::new(0.0, -1.0, 0.0)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pub(crate) fn get_true_world_position(
|
||||
offset: &WorldOffset,
|
||||
transform: &DoubleTransform,
|
||||
) -> DVec3 {
|
||||
transform.translation + offset.0
|
||||
}
|
||||
|
||||
pub fn setup(mut commands: Commands) {
|
||||
commands
|
||||
.spawn((
|
||||
DoubleTransform {
|
||||
translation: DVec3::new(100_000.0, 0.0, 0.0),
|
||||
rotation: DQuat::IDENTITY,
|
||||
scale: DVec3::ONE,
|
||||
},
|
||||
// The standard Bevy Transform (will be updated each frame)
|
||||
Transform::default(),
|
||||
GlobalTransform::default(),
|
||||
// Add your mesh/visibility components, etc.
|
||||
));
|
||||
|
||||
}
|
||||
|
||||
|
||||
pub fn update_render_transform_system(
|
||||
camera_query: Query<&DoubleTransform, With<Camera>>,
|
||||
mut query: Query<(&DoubleTransform, &mut Transform), Without<Camera>>,
|
||||
) {
|
||||
let camera_double_tf = camera_query.single();
|
||||
// The camera offset in double-precision
|
||||
let camera_pos = camera_double_tf.translation;
|
||||
|
||||
for (double_tf, mut transform) in query.iter_mut() {
|
||||
// relative position (double precision)
|
||||
let relative_pos = double_tf.translation - camera_pos;
|
||||
transform.translation = relative_pos.as_vec3(); // convert f64 -> f32
|
||||
transform.rotation = double_tf.rotation.as_quat(); // f64 -> f32
|
||||
transform.scale = double_tf.scale.as_vec3(); // f64 -> f32
|
||||
}
|
||||
}
|
||||
|
||||
pub fn floating_origin_system(
|
||||
mut query: Query<&mut DoubleTransform, Without<Camera>>,
|
||||
mut camera_query: Query<&mut DoubleTransform, With<Camera>>,
|
||||
mut offset: ResMut<WorldOffset>,
|
||||
) {
|
||||
let mut camera_tf = camera_query.single_mut();
|
||||
let camera_pos = camera_tf.translation;
|
||||
|
||||
// If the camera moves any distance, recenter it
|
||||
if camera_pos.length() > 0.001 {
|
||||
offset.0 += camera_pos;
|
||||
// Shift everything so camera ends up back at zero
|
||||
for mut dtf in query.iter_mut() {
|
||||
dtf.translation -= camera_pos;
|
||||
}
|
||||
camera_tf.translation = DVec3::ZERO;
|
||||
}
|
||||
}
|
||||
3
src/helper/mod.rs
Normal file
3
src/helper/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod egui_dock;
|
||||
pub mod debug_gizmos;
|
||||
pub mod large_transform;
|
||||
89
src/main.rs
Normal file
89
src/main.rs
Normal file
@ -0,0 +1,89 @@
|
||||
mod systems;
|
||||
mod plugins;
|
||||
mod app;
|
||||
mod helper;
|
||||
use bevy::DefaultPlugins;
|
||||
use bevy::gizmos::{AppGizmoBuilder, GizmoPlugin};
|
||||
use bevy::log::info;
|
||||
use bevy::prelude::{default, App, GizmoConfigGroup, PluginGroup, Reflect, Res, Resource};
|
||||
use bevy::render::RenderPlugin;
|
||||
use bevy::render::settings::{Backends, RenderCreation, WgpuSettings};
|
||||
use bevy_egui::{EguiPlugin};
|
||||
use bevy_inspector_egui::DefaultInspectorConfigPlugin;
|
||||
use bevy_window::{PresentMode, Window, WindowPlugin};
|
||||
use crate::app::AppPlugin;
|
||||
|
||||
const TITLE: &str = "Fluid Simulation";
|
||||
const RESOLUTION: (f32,f32) = (1920f32, 1080f32);
|
||||
const RESIZABLE: bool = true;
|
||||
const DECORATIONS: bool = true;
|
||||
const TRANSPARENT: bool = true;
|
||||
const PRESENT_MODE: PresentMode = PresentMode::AutoVsync;
|
||||
|
||||
|
||||
|
||||
fn main() {
|
||||
let mut app = App::new();
|
||||
register_platform_plugins(&mut app);
|
||||
|
||||
app.add_plugins(AppPlugin);
|
||||
app.add_plugins(EguiPlugin);
|
||||
app.add_plugins(DefaultInspectorConfigPlugin);
|
||||
/*app.add_plugins(GizmoPlugin);*/
|
||||
|
||||
app.run();
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct InspectorVisible(bool);
|
||||
fn register_platform_plugins(app: &mut App) {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// Register Windows-specific plugins
|
||||
info!("Adding Windows-specific plugins");
|
||||
app.add_plugins(DefaultPlugins
|
||||
.set(RenderPlugin {
|
||||
render_creation: RenderCreation::Automatic(WgpuSettings {
|
||||
backends: Some(Backends::VULKAN),
|
||||
..default()
|
||||
}),
|
||||
..default()
|
||||
})
|
||||
.set(WindowPlugin {
|
||||
primary_window: Some(Window {
|
||||
title: TITLE.to_string(), // Window title
|
||||
resolution: RESOLUTION.into(), // Initial resolution (width x height)
|
||||
resizable: RESIZABLE, // Allow resizing
|
||||
decorations: DECORATIONS, // Enable window decorations
|
||||
transparent: TRANSPARENT, // Opaque background
|
||||
present_mode: PRESENT_MODE, // VSync mode
|
||||
..default()
|
||||
}),
|
||||
..default()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
info!("Adding macOS-specific plugins");
|
||||
app.add_plugins(DefaultPlugins)
|
||||
.set(WindowPlugin {
|
||||
primary_window: Some(Window {
|
||||
title: crate::TITLE.to_string(), // Window title
|
||||
resolution: crate::RESOLUTION.into(), // Initial resolution (width x height)
|
||||
resizable: crate::RESIZABLE, // Allow resizing
|
||||
decorations: crate::DECORATIONS, // Enable window decorations
|
||||
transparent: crate::TRANSPARENT, // Opaque background
|
||||
present_mode: crate::PRESENT_MODE, // VSync mode
|
||||
..default()
|
||||
}),
|
||||
..default()
|
||||
});
|
||||
}
|
||||
}
|
||||
fn should_display_inspector(inspector_visible: Res<InspectorVisible>) -> bool {
|
||||
inspector_visible.0
|
||||
}
|
||||
12
src/plugins/camera_plugin.rs
Normal file
12
src/plugins/camera_plugin.rs
Normal file
@ -0,0 +1,12 @@
|
||||
use bevy::a11y::AccessibilitySystem::Update;
|
||||
use bevy::app::{App, Plugin, PreUpdate, Startup};
|
||||
|
||||
pub struct CameraPlugin;
|
||||
impl Plugin for CameraPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(Startup, (crate::systems::camera_system::setup));
|
||||
app.add_systems(PreUpdate, (crate::systems::camera_system::camera_controller_system));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
37
src/plugins/environment_plugin.rs
Normal file
37
src/plugins/environment_plugin.rs
Normal file
@ -0,0 +1,37 @@
|
||||
use std::fs::create_dir;
|
||||
use bevy::app::{App, Plugin, PreUpdate, Startup};
|
||||
use bevy::color::palettes::css::{GRAY, RED};
|
||||
use bevy::prelude::{default, Color, Commands, GlobalTransform, IntoSystemConfigs, Query, Res, Update};
|
||||
use bevy_render::prelude::ClearColor;
|
||||
use crate::app::InspectorVisible;
|
||||
use crate::systems::environment_system::*;
|
||||
use crate::systems::voxels::structure::{ChunkEntities, SparseVoxelOctree, Voxel};
|
||||
|
||||
pub struct EnvironmentPlugin;
|
||||
impl Plugin for EnvironmentPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
/*app.insert_resource(ClearColor(Color::from(GRAY)));*/
|
||||
app.init_resource::<ChunkEntities>();
|
||||
app.add_systems(Startup, (setup).chain());
|
||||
app.add_systems(Update, (crate::systems::voxels::rendering::render,crate::systems::voxels::debug::visualize_octree.run_if(should_visualize_octree), crate::systems::voxels::debug::draw_grid.run_if(should_draw_grid), crate::systems::voxels::debug::debug_draw_chunks_system.run_if(should_visualize_chunks)).chain());
|
||||
|
||||
app.register_type::<SparseVoxelOctree>();
|
||||
app.register_type::<ChunkEntities>();
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
fn should_visualize_octree(octree_query: Query<&SparseVoxelOctree>,) -> bool {
|
||||
octree_query.single().show_wireframe
|
||||
}
|
||||
|
||||
fn should_draw_grid(octree_query: Query<&SparseVoxelOctree>,) -> bool {
|
||||
octree_query.single().show_world_grid
|
||||
}
|
||||
|
||||
fn should_visualize_chunks(octree_query: Query<&SparseVoxelOctree>,) -> bool {
|
||||
octree_query.single().show_chunks
|
||||
}
|
||||
17
src/plugins/large_transform_plugin.rs
Normal file
17
src/plugins/large_transform_plugin.rs
Normal file
@ -0,0 +1,17 @@
|
||||
|
||||
use bevy::app::{App, Plugin, PreUpdate, Startup, Update};
|
||||
use bevy::prelude::IntoSystemConfigs;
|
||||
use crate::helper::large_transform::*;
|
||||
|
||||
pub struct LargeTransformPlugin;
|
||||
impl Plugin for LargeTransformPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
|
||||
app.insert_resource(WorldOffset::default());
|
||||
app.add_systems(Startup, setup);
|
||||
app.add_systems(Update, floating_origin_system.after(crate::systems::camera_system::camera_controller_system));
|
||||
app.add_systems(Update, update_render_transform_system.after(floating_origin_system));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
5
src/plugins/mod.rs
Normal file
5
src/plugins/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
|
||||
pub mod large_transform_plugin;
|
||||
pub mod camera_plugin;
|
||||
pub mod ui_plugin;
|
||||
pub mod environment_plugin;
|
||||
15
src/plugins/ui_plugin.rs
Normal file
15
src/plugins/ui_plugin.rs
Normal file
@ -0,0 +1,15 @@
|
||||
|
||||
|
||||
use bevy::app::{App, FixedUpdate, Plugin, PreUpdate, Startup};
|
||||
use bevy::prelude::IntoSystemConfigs;
|
||||
use crate::systems::ui_system::*;
|
||||
|
||||
pub struct UiPlugin;
|
||||
impl Plugin for UiPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(Startup, setup);
|
||||
app.add_systems(FixedUpdate, update);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
318
src/systems/camera_system.rs
Normal file
318
src/systems/camera_system.rs
Normal file
@ -0,0 +1,318 @@
|
||||
use bevy::color::palettes::basic::{BLUE, GREEN};
|
||||
use bevy::input::mouse::{MouseMotion, MouseWheel};
|
||||
use bevy::math::{DQuat, DVec3, Vec3};
|
||||
use bevy::prelude::*;
|
||||
use bevy_render::camera::{OrthographicProjection, Projection, ScalingMode};
|
||||
use bevy_window::CursorGrabMode;
|
||||
use crate::helper::egui_dock::MainCamera;
|
||||
use crate::helper::large_transform::{DoubleTransform, WorldOffset};
|
||||
use crate::InspectorVisible;
|
||||
use crate::systems::voxels::structure::{Ray, SparseVoxelOctree, Voxel};
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct CameraController {
|
||||
pub yaw: f32,
|
||||
pub pitch: f32,
|
||||
pub speed: f32,
|
||||
pub sensitivity: f32,
|
||||
}
|
||||
|
||||
impl Default for CameraController {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
yaw: 0.0,
|
||||
pitch: 0.0,
|
||||
speed: 10.0,
|
||||
sensitivity: 0.1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setup(mut commands: Commands,){
|
||||
|
||||
|
||||
|
||||
commands.spawn((
|
||||
DoubleTransform {
|
||||
translation: DVec3::new(0.0, 0.0, 10.0),
|
||||
rotation: DQuat::IDENTITY,
|
||||
scale: DVec3::ONE,
|
||||
},
|
||||
Transform::from_xyz(0.0, 5.0, 10.0), // initial f32
|
||||
GlobalTransform::default(),
|
||||
Camera3d::default(),
|
||||
Projection::from(PerspectiveProjection{
|
||||
near: 0.0001,
|
||||
..default()
|
||||
}),
|
||||
MainCamera,
|
||||
CameraController::default()
|
||||
|
||||
));
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// Example system to control a camera using double-precision for position.
|
||||
pub fn camera_controller_system(
|
||||
time: Res<Time>,
|
||||
keyboard_input: Res<ButtonInput<KeyCode>>,
|
||||
mouse_button_input: Res<ButtonInput<MouseButton>>,
|
||||
mut mouse_motion_events: EventReader<MouseMotion>,
|
||||
mut mouse_wheel_events: EventReader<MouseWheel>,
|
||||
mut windows: Query<&mut Window>,
|
||||
// Here we query for BOTH DoubleTransform (f64) and Transform (f32).
|
||||
// We'll update DoubleTransform for the "true" position
|
||||
// and keep Transform in sync for rendering.a
|
||||
mut query: Query<(&mut DoubleTransform, &mut Transform, &mut CameraController)>,
|
||||
mut octree_query: Query<&mut SparseVoxelOctree>,
|
||||
mut app_exit_events: EventWriter<AppExit>,
|
||||
world_offset: Res<WorldOffset>,
|
||||
) {
|
||||
let mut window = windows.single_mut();
|
||||
let (mut double_tf, mut render_tf, mut controller) = query.single_mut();
|
||||
|
||||
// ====================
|
||||
// 1) Handle Mouse Look
|
||||
// ====================
|
||||
if !window.cursor_options.visible {
|
||||
for event in mouse_motion_events.read() {
|
||||
// Adjust yaw/pitch in f32
|
||||
controller.yaw -= event.delta.x * controller.sensitivity;
|
||||
controller.pitch += event.delta.y * controller.sensitivity;
|
||||
controller.pitch = controller.pitch.clamp(-89.9, 89.9);
|
||||
|
||||
// Convert degrees to radians (f32)
|
||||
let yaw_radians = controller.yaw.to_radians();
|
||||
let pitch_radians = controller.pitch.to_radians();
|
||||
|
||||
// Build a double-precision quaternion from those angles
|
||||
let rot_yaw = DQuat::from_axis_angle(DVec3::Y, yaw_radians as f64);
|
||||
let rot_pitch = DQuat::from_axis_angle(DVec3::X, -pitch_radians as f64);
|
||||
|
||||
double_tf.rotation = rot_yaw * rot_pitch;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
// 2) Adjust Movement Speed with Mouse Wheel
|
||||
// ====================
|
||||
for event in mouse_wheel_events.read() {
|
||||
let base_factor = 1.1_f32;
|
||||
let factor = base_factor.powf(event.y);
|
||||
controller.speed *= factor;
|
||||
if controller.speed < 0.01 {
|
||||
controller.speed = 0.01;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
// 3) Handle Keyboard Movement (WASD, Space, Shift)
|
||||
// ====================
|
||||
let mut direction = DVec3::ZERO;
|
||||
|
||||
// Forward/Back
|
||||
if keyboard_input.pressed(KeyCode::KeyW) {
|
||||
direction += double_tf.forward();
|
||||
}
|
||||
if keyboard_input.pressed(KeyCode::KeyS) {
|
||||
direction -= double_tf.forward();
|
||||
}
|
||||
|
||||
// Left/Right
|
||||
if keyboard_input.pressed(KeyCode::KeyA) {
|
||||
direction -= double_tf.right();
|
||||
}
|
||||
if keyboard_input.pressed(KeyCode::KeyD) {
|
||||
direction += double_tf.right();
|
||||
}
|
||||
|
||||
// Up/Down
|
||||
if keyboard_input.pressed(KeyCode::Space) {
|
||||
direction += double_tf.up();
|
||||
}
|
||||
if keyboard_input.pressed(KeyCode::ShiftLeft) || keyboard_input.pressed(KeyCode::ShiftRight) {
|
||||
direction -= double_tf.up();
|
||||
}
|
||||
|
||||
// Normalize direction if needed
|
||||
if direction.length_squared() > 0.0 {
|
||||
direction = direction.normalize();
|
||||
}
|
||||
|
||||
// Apply movement in double-precision
|
||||
let delta_seconds = time.delta_secs_f64();
|
||||
let distance = controller.speed as f64 * delta_seconds;
|
||||
double_tf.translation += direction * distance;
|
||||
|
||||
|
||||
|
||||
// =========================
|
||||
// 4) Lock/Unlock Mouse (L)
|
||||
// =========================
|
||||
if keyboard_input.just_pressed(KeyCode::KeyL) {
|
||||
// Toggle between locked and unlocked
|
||||
if window.cursor_options.grab_mode == CursorGrabMode::None {
|
||||
// Lock
|
||||
window.cursor_options.visible = false;
|
||||
window.cursor_options.grab_mode = CursorGrabMode::Locked;
|
||||
} else {
|
||||
// Unlock
|
||||
window.cursor_options.visible = true;
|
||||
window.cursor_options.grab_mode = CursorGrabMode::None;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =======================
|
||||
// 5) Octree Keys
|
||||
// =======================
|
||||
if keyboard_input.just_pressed(KeyCode::F2){
|
||||
for mut octree in octree_query.iter_mut() {
|
||||
octree.show_wireframe = !octree.show_wireframe;
|
||||
}
|
||||
}
|
||||
if keyboard_input.just_pressed(KeyCode::F3){
|
||||
for mut octree in octree_query.iter_mut() {
|
||||
octree.show_world_grid = !octree.show_world_grid;
|
||||
}
|
||||
}
|
||||
if keyboard_input.just_pressed(KeyCode::F4){
|
||||
for mut octree in octree_query.iter_mut() {
|
||||
octree.show_chunks = !octree.show_chunks;
|
||||
}
|
||||
}
|
||||
if keyboard_input.just_pressed(KeyCode::KeyQ) && window.cursor_options.visible == false{
|
||||
for mut octree in octree_query.iter_mut() {
|
||||
octree.insert(double_tf.translation.x as f64, double_tf.translation.y as f64, double_tf.translation.z as f64, Voxel::new(Color::srgb(1.0, 0.0, 0.0)));
|
||||
}
|
||||
}
|
||||
|
||||
// =======================
|
||||
// 6) Building
|
||||
// =======================
|
||||
|
||||
if (mouse_button_input.just_pressed(MouseButton::Left) || mouse_button_input.just_pressed(MouseButton::Right)) && !window.cursor_options.visible {
|
||||
|
||||
// Get the mouse position in normalized device coordinates (-1 to 1)
|
||||
if let Some(_) = window.cursor_position() {
|
||||
// Set the ray direction to the camera's forward vector
|
||||
let ray_origin = world_offset.0 + double_tf.translation;
|
||||
let ray_direction = double_tf.forward().normalize();
|
||||
|
||||
let ray = Ray {
|
||||
origin: ray_origin.as_vec3(),
|
||||
direction: ray_direction.as_vec3(),
|
||||
};
|
||||
|
||||
|
||||
|
||||
for mut octree in octree_query.iter_mut() {
|
||||
if let Some((hit_x, hit_y, hit_z, depth,normal)) = octree.raycast(&ray) {
|
||||
|
||||
|
||||
|
||||
/*//TODO: Currently broken needs fixing to work with double precision
|
||||
println!("raycast: {:?}", ray);
|
||||
// Visualize the ray
|
||||
lines.lines.push(EphemeralLine {
|
||||
start: ray_origin.as_vec3(),
|
||||
end: DVec3::new(hit_x, hit_y, hit_z).as_vec3(),
|
||||
color: Color::from(GREEN),
|
||||
time_left: 5.0, // draw for 2 seconds
|
||||
});*/
|
||||
|
||||
/*gizmos.ray(
|
||||
ray.origin,
|
||||
ray.direction,
|
||||
BLUE,
|
||||
);*/
|
||||
|
||||
let chunk = octree.compute_chunk_coords(hit_x, hit_y, hit_z);
|
||||
|
||||
info!("Chunk Hit: {},{},{}", chunk.0, chunk.1, chunk.2);
|
||||
|
||||
if let Some(chunk_node) = octree.get_chunk_node(hit_x,hit_y,hit_z) {
|
||||
let has_volume = octree.has_volume(chunk_node);
|
||||
|
||||
info!("Chunk Has Volume: {}", has_volume);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if mouse_button_input.just_pressed(MouseButton::Right) {
|
||||
|
||||
let voxel_size = octree.get_spacing_at_depth(depth);
|
||||
let hit_position = Vec3::new(hit_x as f32, hit_y as f32, hit_z as f32);
|
||||
let epsilon = voxel_size * 0.1; // Adjust this value as needed (e.g., 0.1 times the voxel size)
|
||||
|
||||
// Offset position by epsilon in the direction of the normal
|
||||
let offset_position = hit_position - (normal * Vec3::new(epsilon as f32, epsilon as f32, epsilon as f32));
|
||||
|
||||
// Align the offset position to the center of the nearest voxel
|
||||
let (new_voxel_x, new_voxel_y, new_voxel_z) = octree.normalize_to_voxel_at_depth(
|
||||
offset_position.x as f64,
|
||||
offset_position.y as f64,
|
||||
offset_position.z as f64,
|
||||
depth,
|
||||
);
|
||||
|
||||
// Remove the voxel
|
||||
octree.remove(new_voxel_x, new_voxel_y, new_voxel_z);
|
||||
}
|
||||
else if mouse_button_input.just_pressed(MouseButton::Left) {
|
||||
|
||||
let voxel_size = octree.get_spacing_at_depth(depth);
|
||||
let hit_position = Vec3::new(hit_x as f32, hit_y as f32, hit_z as f32);
|
||||
let epsilon = voxel_size * 0.1; // Adjust this value as needed (e.g., 0.1 times the voxel size)
|
||||
|
||||
// Offset position by epsilon in the direction of the normal
|
||||
let offset_position = hit_position + (normal * Vec3::new(epsilon as f32, epsilon as f32, epsilon as f32));
|
||||
|
||||
// Align the offset position to the center of the nearest voxel
|
||||
let (new_voxel_x, new_voxel_y, new_voxel_z) = octree.normalize_to_voxel_at_depth(
|
||||
offset_position.x as f64,
|
||||
offset_position.y as f64,
|
||||
offset_position.z as f64,
|
||||
depth,
|
||||
);
|
||||
|
||||
// Insert the new voxel
|
||||
octree.insert(
|
||||
new_voxel_x,
|
||||
new_voxel_y,
|
||||
new_voxel_z,
|
||||
Voxel::new(Color::srgb(1.0, 0.0, 0.0)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// =======================
|
||||
// 7) Exit on Escape
|
||||
// =======================
|
||||
if keyboard_input.pressed(KeyCode::Escape) {
|
||||
app_exit_events.send(Default::default());
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// 8) Convert DoubleTransform -> Bevy Transform
|
||||
// =============================================
|
||||
// The final step is to update the f32 `Transform` that Bevy uses for rendering.
|
||||
// This ensures the camera is visually placed at the correct position.
|
||||
render_tf.translation = double_tf.translation.as_vec3();
|
||||
render_tf.rotation = double_tf.rotation.as_quat();
|
||||
render_tf.scale = double_tf.scale.as_vec3();
|
||||
|
||||
|
||||
}
|
||||
245
src/systems/environment_system.rs
Normal file
245
src/systems/environment_system.rs
Normal file
@ -0,0 +1,245 @@
|
||||
use bevy::color::palettes::basic::*;
|
||||
use bevy::color::palettes::css::{BEIGE, MIDNIGHT_BLUE, ORANGE, ORANGE_RED, SEA_GREEN};
|
||||
use bevy::math::*;
|
||||
use bevy::prelude::*;
|
||||
use crate::helper::large_transform::DoubleTransform;
|
||||
use crate::systems::voxels::structure::{SparseVoxelOctree, Voxel};
|
||||
/*pub fn setup(
|
||||
mut commands: Commands,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
){
|
||||
// 1) Circular base
|
||||
commands.spawn((
|
||||
// Double precision
|
||||
DoubleTransform {
|
||||
translation: DVec3::new(0.0, 0.0, 10.0),
|
||||
// rotate -90 degrees around X so the circle is on the XY plane
|
||||
rotation: DQuat::from_euler(EulerRot::XYZ, -std::f64::consts::FRAC_PI_2, 0.0, 0.0),
|
||||
scale: DVec3::ONE,
|
||||
},
|
||||
// Bevy's transform components
|
||||
Transform::default(),
|
||||
GlobalTransform::default(),
|
||||
// 3D mesh + material
|
||||
Mesh3d(meshes.add(Circle::new(4.0))),
|
||||
MeshMaterial3d(materials.add(Color::WHITE)),
|
||||
));
|
||||
|
||||
// 2) Cube
|
||||
commands.spawn((
|
||||
// Double precision
|
||||
DoubleTransform {
|
||||
translation: DVec3::new(0.0, 0.5, 10.0),
|
||||
rotation: DQuat::IDENTITY,
|
||||
scale: DVec3::ONE,
|
||||
},
|
||||
// Bevy's transform components
|
||||
Transform::default(),
|
||||
GlobalTransform::default(),
|
||||
// 3D mesh + material
|
||||
Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
|
||||
MeshMaterial3d(materials.add(Color::rgb_u8(124, 144, 255))),
|
||||
));
|
||||
|
||||
// 3) Point light
|
||||
commands.spawn((
|
||||
DoubleTransform {
|
||||
translation: DVec3::new(4.0, 8.0, 14.0),
|
||||
rotation: DQuat::IDENTITY,
|
||||
scale: DVec3::ONE,
|
||||
},
|
||||
Transform::default(),
|
||||
GlobalTransform::default(),
|
||||
PointLight {
|
||||
shadows_enabled: true,
|
||||
..default()
|
||||
},
|
||||
));
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
pub fn setup(mut commands: Commands,) {
|
||||
|
||||
|
||||
let voxels_per_unit = 16;
|
||||
let unit_size = 1.0; // 1 unit in your coordinate space
|
||||
let voxel_size = unit_size / voxels_per_unit as f64;
|
||||
|
||||
/*//Octree
|
||||
let octree_base_size = 64.0;
|
||||
let octree_depth = 10;*/
|
||||
|
||||
// Octree parameters
|
||||
let octree_base_size = 64.0 * unit_size; // Octree's total size in your world space
|
||||
let octree_depth = 10;
|
||||
|
||||
|
||||
let mut octree = SparseVoxelOctree::new(octree_depth, octree_base_size, false, false, false);
|
||||
|
||||
|
||||
let color = Color::rgb(0.2, 0.8, 0.2);
|
||||
/*generate_voxel_rect(&mut octree,color);*/
|
||||
/*generate_voxel_sphere(&mut octree, 10.0, color);*/
|
||||
|
||||
generate_large_plane(&mut octree, 200, 200,color );
|
||||
|
||||
/*octree.insert(0.0,0.0,0.0, Voxel::new(Color::from(RED)));*/
|
||||
|
||||
|
||||
commands.spawn(
|
||||
(
|
||||
DoubleTransform {
|
||||
translation: DVec3::new(0.0, 0.0, 0.0),
|
||||
rotation: DQuat::IDENTITY,
|
||||
scale: DVec3::ONE,
|
||||
},
|
||||
Transform::default(),
|
||||
octree
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
commands.spawn((
|
||||
Transform::default(),
|
||||
GlobalTransform::default(),
|
||||
DoubleTransform {
|
||||
translation: DVec3::new(0.0, 0.0, 0.0),
|
||||
rotation: DQuat::IDENTITY,
|
||||
scale: DVec3::ONE,
|
||||
},
|
||||
PointLight {
|
||||
shadows_enabled: true,
|
||||
..default()
|
||||
},
|
||||
));
|
||||
|
||||
// Insert the octree into the ECS
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Naïve function to generate a spherical planet in the voxel octree.
|
||||
/// - `planet_radius`: radius of the "planet" in your world-space units
|
||||
/// - `voxel_step`: how finely to sample the sphere in the x/y/z loops
|
||||
fn generate_voxel_sphere(
|
||||
octree: &mut SparseVoxelOctree,
|
||||
planet_radius: f64,
|
||||
voxel_color: Color,
|
||||
) {
|
||||
// For simplicity, we center the sphere around (0,0,0).
|
||||
// We'll loop over a cubic region [-planet_radius, +planet_radius] in x, y, z
|
||||
let min = -(planet_radius as i64);
|
||||
let max = planet_radius as i64;
|
||||
|
||||
let step = octree.get_spacing_at_depth(octree.max_depth);
|
||||
|
||||
for ix in min..=max {
|
||||
let x = ix as f64;
|
||||
for iy in min..=max {
|
||||
let y = iy as f64;
|
||||
for iz in min..=max {
|
||||
let z = iz as f64;
|
||||
|
||||
// Check if within sphere of radius `planet_radius`
|
||||
let dist2 = x * x + y * y + z * z;
|
||||
if dist2 <= planet_radius * planet_radius {
|
||||
// Convert (x,y,z) to world space, stepping by `voxel_step`.
|
||||
let wx = x * step;
|
||||
let wy = y * step;
|
||||
let wz = z * step;
|
||||
|
||||
// Insert the voxel
|
||||
let voxel = Voxel {
|
||||
color: voxel_color,
|
||||
position: Default::default(), // Will get set internally by `insert()`
|
||||
};
|
||||
octree.insert(wx, wy, wz, voxel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Inserts a 16x256x16 "column" of voxels into the octree at (0,0,0) corner.
|
||||
/// If you want it offset or centered differently, just adjust the for-loop ranges or offsets.
|
||||
fn generate_voxel_rect(
|
||||
octree: &mut SparseVoxelOctree,
|
||||
voxel_color: Color,
|
||||
) {
|
||||
// The dimensions of our rectangle: 16 x 256 x 16
|
||||
let size_x = 16;
|
||||
let size_y = 256;
|
||||
let size_z = 16;
|
||||
|
||||
// We'll get the voxel spacing (size at the deepest level), same as in your sphere code.
|
||||
let step = octree.get_spacing_at_depth(octree.max_depth);
|
||||
|
||||
// Triple-nested loop for each voxel in [0..16, 0..256, 0..16]
|
||||
for ix in 0..size_x {
|
||||
let x = ix as f64;
|
||||
for iy in 0..size_y {
|
||||
let y = iy as f64;
|
||||
for iz in 0..size_z {
|
||||
let z = iz as f64;
|
||||
|
||||
// Convert (x,y,z) to world coordinates
|
||||
let wx = x * step;
|
||||
let wy = y * step;
|
||||
let wz = z * step;
|
||||
|
||||
// Create the voxel
|
||||
let voxel = Voxel {
|
||||
color: voxel_color,
|
||||
position: Default::default(), // Will be set by octree internally
|
||||
};
|
||||
|
||||
// Insert the voxel into the octree
|
||||
octree.insert(wx, wy, wz, voxel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_large_plane(
|
||||
octree: &mut SparseVoxelOctree,
|
||||
width: usize,
|
||||
depth: usize,
|
||||
color: Color,
|
||||
) {
|
||||
// We'll get the voxel spacing (size at the deepest level).
|
||||
let step = octree.get_spacing_at_depth(octree.max_depth);
|
||||
|
||||
// Double-nested loop for each voxel in [0..width, 0..depth],
|
||||
// with y=0.
|
||||
for ix in 0..width {
|
||||
let x = ix as f64;
|
||||
for iz in 0..depth {
|
||||
let z = iz as f64;
|
||||
// y is always 0.
|
||||
let y = 0.0;
|
||||
|
||||
// Convert (x,0,z) to world coordinates
|
||||
let wx = x * step;
|
||||
let wy = y * step;
|
||||
let wz = z * step;
|
||||
|
||||
// Create the voxel
|
||||
let voxel = Voxel {
|
||||
color,
|
||||
position: Vec3::ZERO, // Will be set internally by octree.insert()
|
||||
};
|
||||
|
||||
// Insert the voxel into the octree
|
||||
octree.insert(wx, wy, wz, voxel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
5
src/systems/mod.rs
Normal file
5
src/systems/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
pub mod camera_system;
|
||||
|
||||
pub mod ui_system;
|
||||
pub mod environment_system;
|
||||
pub mod voxels;
|
||||
90
src/systems/ui_system.rs
Normal file
90
src/systems/ui_system.rs
Normal file
@ -0,0 +1,90 @@
|
||||
use bevy::asset::AssetServer;
|
||||
use bevy::prelude::*;
|
||||
use crate::helper::large_transform::{DoubleTransform, WorldOffset};
|
||||
use crate::systems::camera_system::CameraController;
|
||||
use crate::systems::voxels::structure::{SparseVoxelOctree};
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct SpeedDisplay;
|
||||
|
||||
/// Spawns a UI Text entity to display speed/positions.
|
||||
pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||
// Use the new UI API, or the old UI Node-based system.
|
||||
// This example uses an older approach to Node/Style, but can be adapted to `TextBundle`.
|
||||
// If you're on Bevy 0.11+, you can also do `TextBundle::from_section(...)`.
|
||||
commands.spawn((
|
||||
// The text to display:
|
||||
Text::new("Speed: 0.0"),
|
||||
// The font, loaded from an asset file
|
||||
TextFont {
|
||||
font: asset_server.load("fonts/minecraft_font.ttf"),
|
||||
font_size: 25.0,
|
||||
..default()
|
||||
},
|
||||
// The text layout style
|
||||
TextLayout::new_with_justify(JustifyText::Left),
|
||||
// Style for positioning the UI node
|
||||
Node {
|
||||
position_type: PositionType::Relative,
|
||||
bottom: Val::Px(9.0),
|
||||
right: Val::Px(9.0),
|
||||
..default()
|
||||
},
|
||||
// Our marker so we can query this entity
|
||||
SpeedDisplay,
|
||||
));
|
||||
}
|
||||
|
||||
/// System that updates the UI text each frame with
|
||||
/// - speed
|
||||
/// - camera f32 position
|
||||
/// - camera global f64 position
|
||||
/// - current chunk coordinate
|
||||
pub fn update(
|
||||
// Query the camera controller so we can see its speed
|
||||
query_camera_controller: Query<&CameraController>,
|
||||
// We also query for the camera's f32 `Transform` and the double `DoubleTransform`
|
||||
camera_query: Query<(&Transform, &DoubleTransform, &Camera)>,
|
||||
// The global offset resource, if you have one
|
||||
world_offset: Res<WorldOffset>,
|
||||
// The chunk-size logic from the octree, so we can compute chunk coords
|
||||
octree_query: Query<&SparseVoxelOctree>, // or get_single if there's only one octree
|
||||
|
||||
// The UI text entity
|
||||
mut query_text: Query<&mut Text, With<SpeedDisplay>>,
|
||||
) {
|
||||
let camera_controller = query_camera_controller.single();
|
||||
let (transform, double_tf, _camera) = camera_query.single();
|
||||
let mut text = query_text.single_mut();
|
||||
|
||||
// The global double position: offset + camera's double translation
|
||||
let global_pos = world_offset.0 + double_tf.translation;
|
||||
|
||||
// We'll attempt to get the octree so we can compute chunk coords
|
||||
// If there's no octree, we just show "N/A".
|
||||
/*let (chunk_cx, chunk_cy, chunk_cz) = if let Ok(octree) = octree_query.get_single() {
|
||||
// 1) get voxel step
|
||||
let step = octree.get_spacing_at_depth(octree.max_depth);
|
||||
// 2) chunk world size
|
||||
let chunk_world_size = CHUNK_SIZE as f64 * step;
|
||||
// 3) compute chunk coords using global_pos
|
||||
let cx = ((global_pos.x) / chunk_world_size).floor() as i32;
|
||||
let cy = ((global_pos.y) / chunk_world_size).floor() as i32;
|
||||
let cz = ((global_pos.z) / chunk_world_size).floor() as i32;
|
||||
(cx, cy, cz)
|
||||
} else {
|
||||
(0, 0, 0) // or default
|
||||
};*/
|
||||
|
||||
// Format the string to show speed, positions, and chunk coords
|
||||
text.0 = format!(
|
||||
"\n Speed: {:.3}\n Position(f32): ({:.2},{:.2},{:.2})\n Position(f64): ({:.2},{:.2},{:.2})",
|
||||
camera_controller.speed,
|
||||
transform.translation.x,
|
||||
transform.translation.y,
|
||||
transform.translation.z,
|
||||
global_pos.x,
|
||||
global_pos.y,
|
||||
global_pos.z,
|
||||
);
|
||||
}
|
||||
360
src/systems/voxels/debug.rs
Normal file
360
src/systems/voxels/debug.rs
Normal file
@ -0,0 +1,360 @@
|
||||
use bevy::color::palettes::basic::{BLACK, RED, YELLOW};
|
||||
use bevy::color::palettes::css::GREEN;
|
||||
use bevy::math::{DQuat, DVec3, Vec3};
|
||||
use bevy::pbr::wireframe::Wireframe;
|
||||
use bevy::prelude::*;
|
||||
use bevy::render::mesh::{Indices, PrimitiveTopology};
|
||||
use bevy::render::render_asset::RenderAssetUsages;
|
||||
use bevy_egui::egui::emath::Numeric;
|
||||
use bevy_render::prelude::*;
|
||||
use crate::helper::large_transform::DoubleTransform;
|
||||
use crate::systems::voxels::structure::{ChunkEntities, OctreeNode, SparseVoxelOctree};
|
||||
|
||||
pub fn visualize_octree(
|
||||
mut gizmos: Gizmos,
|
||||
camera_query: Query<&DoubleTransform, With<Camera>>,
|
||||
octree_query: Query<(&SparseVoxelOctree, &DoubleTransform)>,
|
||||
) {
|
||||
let camera_tf = camera_query.single(); // your "real" camera position in double precision
|
||||
let camera_pos = camera_tf.translation; // DVec3
|
||||
|
||||
for (octree, octree_tf) in octree_query.iter() {
|
||||
let octree_world_pos = octree_tf.translation;
|
||||
visualize_recursive(
|
||||
&mut gizmos,
|
||||
&octree.root,
|
||||
octree_world_pos, // octree’s root center
|
||||
octree.size,
|
||||
octree.max_depth,
|
||||
camera_pos,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn visualize_recursive(
|
||||
gizmos: &mut Gizmos,
|
||||
node: &OctreeNode,
|
||||
node_center: DVec3,
|
||||
node_size: f64,
|
||||
depth: u32,
|
||||
camera_pos: DVec3,
|
||||
) {
|
||||
if depth == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// If you want to draw the bounding box of this node:
|
||||
/*let half = node_size as f32 * 0.5;*/
|
||||
// Convert double center -> local f32 position
|
||||
let center_f32 = (node_center - camera_pos).as_vec3();
|
||||
|
||||
// A quick approach: draw a wireframe cube by drawing lines for each edge
|
||||
// Or use "cuboid gizmo" methods in future bevy versions that might exist.
|
||||
/*draw_wire_cube(gizmos, center_f32, half, Color::YELLOW);*/
|
||||
|
||||
gizmos.cuboid(
|
||||
Transform::from_translation(center_f32).with_scale(Vec3::splat(node_size as f32)),
|
||||
BLACK,
|
||||
);
|
||||
|
||||
// Recurse children
|
||||
if let Some(children) = &node.children {
|
||||
let child_size = node_size / 2.0;
|
||||
for (i, child) in children.iter().enumerate() {
|
||||
let offset_x = if (i & 1) == 1 { child_size / 2.0 } else { -child_size / 2.0 };
|
||||
let offset_y = if (i & 2) == 2 { child_size / 2.0 } else { -child_size / 2.0 };
|
||||
let offset_z = if (i & 4) == 4 { child_size / 2.0 } else { -child_size / 2.0 };
|
||||
|
||||
let child_center = DVec3::new(
|
||||
node_center.x + offset_x,
|
||||
node_center.y + offset_y,
|
||||
node_center.z + offset_z,
|
||||
);
|
||||
|
||||
visualize_recursive(
|
||||
gizmos,
|
||||
child,
|
||||
child_center,
|
||||
child_size,
|
||||
depth - 1,
|
||||
camera_pos,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn draw_grid(
|
||||
mut gizmos: Gizmos,
|
||||
camera_query: Query<&DoubleTransform, With<Camera>>,
|
||||
octree_query: Query<(&SparseVoxelOctree, &DoubleTransform)>,
|
||||
) {
|
||||
// 1) Get the camera’s double transform for offset
|
||||
let camera_tf = camera_query.single();
|
||||
let camera_pos = camera_tf.translation; // DVec3
|
||||
|
||||
for (octree, octree_dtf) in octree_query.iter() {
|
||||
|
||||
|
||||
// 2) Octree’s double position
|
||||
let octree_pos = octree_dtf.translation; // e.g. [100_000, 0, 0] in double space
|
||||
|
||||
// 3) Compute spacing in f64
|
||||
let grid_spacing = octree.get_spacing_at_depth(octree.max_depth) as f64;
|
||||
let grid_size = (octree.size / grid_spacing) as i32;
|
||||
|
||||
// 4) Start position in local "octree space"
|
||||
// We'll define the bounding region from [-size/2, +size/2]
|
||||
let half_size = octree.size * 0.5;
|
||||
let start_position = -half_size; // f64
|
||||
|
||||
// 5) Loop over lines
|
||||
for i in 0..=grid_size {
|
||||
// i-th line offset
|
||||
let offset = i as f64 * grid_spacing;
|
||||
|
||||
// a) Lines along Z
|
||||
// from (start_position + offset, 0, start_position)
|
||||
// to (start_position + offset, 0, start_position + grid_size * spacing)
|
||||
{
|
||||
let x = start_position + offset;
|
||||
let z1 = start_position;
|
||||
let z2 = start_position + (grid_size as f64 * grid_spacing);
|
||||
|
||||
// Convert these points to "world double" by adding octree_pos
|
||||
let p1_d = DVec3::new(x, 0.0, z1) + octree_pos;
|
||||
let p2_d = DVec3::new(x, 0.0, z2) + octree_pos;
|
||||
|
||||
// Then offset by camera_pos, convert to f32
|
||||
let p1_f32 = (p1_d - camera_pos).as_vec3();
|
||||
let p2_f32 = (p2_d - camera_pos).as_vec3();
|
||||
|
||||
// Draw the line
|
||||
gizmos.line(p1_f32, p2_f32, Color::WHITE);
|
||||
}
|
||||
|
||||
// b) Lines along X
|
||||
// from (start_position, 0, start_position + offset)
|
||||
// to (start_position + grid_size * spacing, 0, start_position + offset)
|
||||
{
|
||||
let z = start_position + offset;
|
||||
let x1 = start_position;
|
||||
let x2 = start_position + (grid_size as f64 * grid_spacing);
|
||||
|
||||
let p1_d = DVec3::new(x1, 0.0, z) + octree_pos;
|
||||
let p2_d = DVec3::new(x2, 0.0, z) + octree_pos;
|
||||
|
||||
let p1_f32 = (p1_d - camera_pos).as_vec3();
|
||||
let p2_f32 = (p2_d - camera_pos).as_vec3();
|
||||
|
||||
gizmos.line(p1_f32, p2_f32, Color::WHITE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*#[derive(Component)]
|
||||
pub struct GridMarker;
|
||||
|
||||
pub fn draw_grid(
|
||||
mut commands: Commands,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
query: Query<(Entity, &SparseVoxelOctree)>, // Query to access the octree
|
||||
grid_query: Query<Entity, With<GridMarker>>, // Query to find existing grid entities
|
||||
) {
|
||||
for (_, octree) in query.iter() {
|
||||
if octree.show_world_grid {
|
||||
// If grid should be shown, check if it already exists
|
||||
if grid_query.iter().next().is_none() {
|
||||
// Grid doesn't exist, so create it
|
||||
let grid_spacing = octree.get_spacing_at_depth(octree.max_depth) as f32; // Get spacing at the specified depth
|
||||
let grid_size = (octree.size / grid_spacing as f64) as i32; // Determine the number of lines needed
|
||||
|
||||
let mut positions = Vec::new();
|
||||
let mut indices = Vec::new();
|
||||
|
||||
// Calculate the start position to center the grid
|
||||
let start_position = -(octree.size as f32 / 2.0);
|
||||
|
||||
// Create lines along the X and Z axes based on calculated spacing
|
||||
for i in 0..=grid_size {
|
||||
// Lines along the Z-axis
|
||||
positions.push([start_position + i as f32 * grid_spacing, 0.0, start_position]);
|
||||
positions.push([start_position + i as f32 * grid_spacing, 0.0, start_position + grid_size as f32 * grid_spacing]);
|
||||
|
||||
// Indices for the Z-axis lines
|
||||
let base_index = (i * 2) as u32;
|
||||
indices.push(base_index);
|
||||
indices.push(base_index + 1);
|
||||
|
||||
// Lines along the X-axis
|
||||
positions.push([start_position, 0.0, start_position + i as f32 * grid_spacing]);
|
||||
positions.push([start_position + grid_size as f32 * grid_spacing, 0.0, start_position + i as f32 * grid_spacing]);
|
||||
|
||||
// Indices for the X-axis lines
|
||||
let base_index_x = ((grid_size + 1 + i) * 2) as u32;
|
||||
indices.push(base_index_x);
|
||||
indices.push(base_index_x + 1);
|
||||
}
|
||||
|
||||
// Create the line mesh
|
||||
let mut mesh = Mesh::new(PrimitiveTopology::LineList, RenderAssetUsages::default());
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
|
||||
mesh.insert_indices(Indices::U32(indices));
|
||||
|
||||
|
||||
let color = bevy::color::Color::srgba(204.0 / 255.0, 0.0, 218.0 / 255.0, 15.0 / 255.0);
|
||||
|
||||
|
||||
// Spawn the entity with the line mesh
|
||||
commands.spawn(PbrBundle {
|
||||
mesh: meshes.add(mesh).into(),
|
||||
material: materials.add(StandardMaterial {
|
||||
base_color: Color::WHITE,
|
||||
unlit: true, // Makes the lines visible without lighting
|
||||
..Default::default()
|
||||
}).into(),
|
||||
transform: Transform::default(),
|
||||
..Default::default()
|
||||
})
|
||||
.insert(GridMarker); // Add a marker component to identify the grid
|
||||
}
|
||||
} else {
|
||||
// If grid should not be shown, remove any existing grid
|
||||
for grid_entity in grid_query.iter() {
|
||||
commands.entity(grid_entity).despawn();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/*#[derive(Component)]
|
||||
pub struct BuildVisualization;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EphemeralLine {
|
||||
pub start: Vec3,
|
||||
pub end: Vec3,
|
||||
pub color: Color,
|
||||
pub time_left: f32, // in seconds
|
||||
}
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
pub struct EphemeralLines {
|
||||
pub lines: Vec<EphemeralLine>,
|
||||
}
|
||||
|
||||
pub fn ephemeral_lines_system(
|
||||
mut lines: ResMut<EphemeralLines>,
|
||||
mut gizmos: Gizmos,
|
||||
time: Res<Time>,
|
||||
) {
|
||||
let dt = time.delta_secs();
|
||||
|
||||
// Retain only those with time_left > 0, and while they're active, draw them
|
||||
lines.lines.retain_mut(|line| {
|
||||
line.time_left -= dt;
|
||||
if line.time_left > 0.0 {
|
||||
// Draw the line with gizmos
|
||||
gizmos.line(line.start, line.end, line.color);
|
||||
// Keep it
|
||||
true
|
||||
} else {
|
||||
// Time’s up, discard
|
||||
false
|
||||
}
|
||||
});
|
||||
}*/
|
||||
|
||||
// System that draws wireframe boxes around each chunk's bounding region.
|
||||
pub fn debug_draw_chunks_system(
|
||||
chunk_entities: Res<ChunkEntities>,
|
||||
|
||||
// If your chunk placement depends on the octree's transform
|
||||
// query that. Otherwise you can skip if they're always at (0,0,0).
|
||||
octree_query: Query<(&SparseVoxelOctree, &DoubleTransform)>,
|
||||
// Optional: If you want large-world offset for camera, we can subtract camera position.
|
||||
// If you don't have floating-origin logic, you can skip this.
|
||||
camera_query: Query<&DoubleTransform, With<Camera>>,
|
||||
|
||||
mut gizmos: Gizmos,
|
||||
) {
|
||||
// We'll get the octree transform offset if we have only one octree.
|
||||
// Adjust if you have multiple.
|
||||
let (octree, octree_tf) = match octree_query.get_single() {
|
||||
Ok(x) => x,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
// 1) Determine the world size of a single voxel
|
||||
let step = octree.get_spacing_at_depth(octree.max_depth);
|
||||
// chunk_size in world units = 16 voxels * step
|
||||
let chunk_size_world = octree.get_chunk_size() as f64 * step;
|
||||
|
||||
// 2) We'll also get the octree's offset in double precision
|
||||
let octree_pos_d = octree_tf.translation;
|
||||
|
||||
// If you want a floating origin approach, subtract the camera's double position:
|
||||
let camera_tf = match camera_query.get_single() {
|
||||
Ok(tf) => tf,
|
||||
Err(_) => return,
|
||||
};
|
||||
let camera_pos_d = camera_tf.translation;
|
||||
|
||||
// For each chunk coordinate
|
||||
for (&(cx, cy, cz), _entity) in chunk_entities.map.iter() {
|
||||
// 4) Chunk bounding box in double precision
|
||||
let chunk_min_d = octree_pos_d
|
||||
+ DVec3::new(
|
||||
cx as f64 * chunk_size_world,
|
||||
cy as f64 * chunk_size_world,
|
||||
cz as f64 * chunk_size_world,
|
||||
);
|
||||
let chunk_max_d = chunk_min_d + DVec3::splat(chunk_size_world);
|
||||
|
||||
// 5) Convert to local f32 near the camera
|
||||
let min_f32 = (chunk_min_d - camera_pos_d).as_vec3();
|
||||
let max_f32 = (chunk_max_d - camera_pos_d).as_vec3();
|
||||
|
||||
// 6) Draw ephemeral lines for the box
|
||||
draw_wire_cube(&mut gizmos, min_f32, max_f32, Color::from(YELLOW));
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to draw a wireframe box from `min` to `max` in ephemeral gizmos.
|
||||
fn draw_wire_cube(
|
||||
gizmos: &mut Gizmos,
|
||||
min: Vec3,
|
||||
max: Vec3,
|
||||
color: Color,
|
||||
) {
|
||||
// corners
|
||||
let c0 = Vec3::new(min.x, min.y, min.z);
|
||||
let c1 = Vec3::new(max.x, min.y, min.z);
|
||||
let c2 = Vec3::new(min.x, max.y, min.z);
|
||||
let c3 = Vec3::new(max.x, max.y, min.z);
|
||||
let c4 = Vec3::new(min.x, min.y, max.z);
|
||||
let c5 = Vec3::new(max.x, min.y, max.z);
|
||||
let c6 = Vec3::new(min.x, max.y, max.z);
|
||||
let c7 = Vec3::new(max.x, max.y, max.z);
|
||||
|
||||
// edges
|
||||
// bottom face
|
||||
gizmos.line(c0, c1, color);
|
||||
gizmos.line(c1, c3, color);
|
||||
gizmos.line(c3, c2, color);
|
||||
gizmos.line(c2, c0, color);
|
||||
// top face
|
||||
gizmos.line(c4, c5, color);
|
||||
gizmos.line(c5, c7, color);
|
||||
gizmos.line(c7, c6, color);
|
||||
gizmos.line(c6, c4, color);
|
||||
// verticals
|
||||
gizmos.line(c0, c4, color);
|
||||
gizmos.line(c1, c5, color);
|
||||
gizmos.line(c2, c6, color);
|
||||
gizmos.line(c3, c7, color);
|
||||
}
|
||||
322
src/systems/voxels/helper.rs
Normal file
322
src/systems/voxels/helper.rs
Normal file
@ -0,0 +1,322 @@
|
||||
use bevy::color::Color;
|
||||
use bevy::math::DVec3;
|
||||
use bevy::prelude::Vec3;
|
||||
use bevy_egui::egui::Key::D;
|
||||
use crate::systems::voxels::structure::{OctreeNode, Ray, SparseVoxelOctree, Voxel, AABB};
|
||||
|
||||
|
||||
impl SparseVoxelOctree {
|
||||
pub fn ray_intersects_aabb(&self,ray: &Ray, aabb: &AABB) -> bool {
|
||||
let inv_dir = 1.0 / ray.direction;
|
||||
let t1 = (aabb.min - ray.origin) * inv_dir;
|
||||
let t2 = (aabb.max - ray.origin) * inv_dir;
|
||||
|
||||
let t_min = t1.min(t2);
|
||||
let t_max = t1.max(t2);
|
||||
|
||||
let t_enter = t_min.max_element();
|
||||
let t_exit = t_max.min_element();
|
||||
|
||||
t_enter <= t_exit && t_exit >= 0.0
|
||||
}
|
||||
|
||||
|
||||
pub fn get_spacing_at_depth(&self, depth: u32) -> f64 {
|
||||
// Ensure the depth does not exceed the maximum depth
|
||||
let effective_depth = depth.min(self.max_depth);
|
||||
|
||||
// Calculate the voxel size at the specified depth
|
||||
self.size / (2_u64.pow(effective_depth)) as f64
|
||||
}
|
||||
|
||||
/// Normalize the world position to the nearest voxel grid position at the specified depth.
|
||||
pub fn normalize_to_voxel_at_depth(
|
||||
&self,
|
||||
world_x: f64,
|
||||
world_y: f64,
|
||||
world_z: f64,
|
||||
depth: u32,
|
||||
) -> (f64, f64, f64) {
|
||||
// Calculate the voxel size at the specified depth
|
||||
let voxel_size = self.get_spacing_at_depth(depth);
|
||||
|
||||
// Align the world position to the center of the voxel
|
||||
let aligned_x = (world_x / voxel_size).floor() * voxel_size + voxel_size / 2.0;
|
||||
let aligned_y = (world_y / voxel_size).floor() * voxel_size + voxel_size / 2.0;
|
||||
let aligned_z = (world_z / voxel_size).floor() * voxel_size + voxel_size / 2.0;
|
||||
|
||||
(aligned_x, aligned_y, aligned_z)
|
||||
}
|
||||
|
||||
pub fn compute_child_bounds(&self, bounds: &AABB, index: usize) -> AABB {
|
||||
let min = bounds.min;
|
||||
let max = bounds.max;
|
||||
let center = (min + max) / 2.0;
|
||||
|
||||
let x_min = if (index & 1) == 0 { min.x } else { center.x };
|
||||
let x_max = if (index & 1) == 0 { center.x } else { max.x };
|
||||
|
||||
let y_min = if (index & 2) == 0 { min.y } else { center.y };
|
||||
let y_max = if (index & 2) == 0 { center.y } else { max.y };
|
||||
|
||||
let z_min = if (index & 4) == 0 { min.z } else { center.z };
|
||||
let z_max = if (index & 4) == 0 { center.z } else { max.z };
|
||||
|
||||
let child_bounds = AABB {
|
||||
min: Vec3::new(x_min, y_min, z_min),
|
||||
max: Vec3::new(x_max, y_max, z_max),
|
||||
};
|
||||
|
||||
child_bounds
|
||||
}
|
||||
|
||||
pub fn ray_intersects_aabb_with_normal(
|
||||
&self,
|
||||
ray: &Ray,
|
||||
aabb: &AABB,
|
||||
) -> Option<(f32, f32, Vec3)> {
|
||||
let inv_dir = 1.0 / ray.direction;
|
||||
|
||||
let t1 = (aabb.min - ray.origin) * inv_dir;
|
||||
let t2 = (aabb.max - ray.origin) * inv_dir;
|
||||
|
||||
let tmin = t1.min(t2);
|
||||
let tmax = t1.max(t2);
|
||||
|
||||
let t_enter = tmin.max_element();
|
||||
let t_exit = tmax.min_element();
|
||||
|
||||
if t_enter <= t_exit && t_exit >= 0.0 {
|
||||
// Calculate normal based on which component contributed to t_enter
|
||||
let epsilon = 1e-6;
|
||||
let mut normal = Vec3::ZERO;
|
||||
|
||||
if (t_enter - t1.x).abs() < epsilon || (t_enter - t2.x).abs() < epsilon {
|
||||
normal = Vec3::new(if ray.direction.x < 0.0 { 1.0 } else { -1.0 }, 0.0, 0.0);
|
||||
} else if (t_enter - t1.y).abs() < epsilon || (t_enter - t2.y).abs() < epsilon {
|
||||
normal = Vec3::new(0.0, if ray.direction.y < 0.0 { 1.0 } else { -1.0 }, 0.0);
|
||||
} else if (t_enter - t1.z).abs() < epsilon || (t_enter - t2.z).abs() < epsilon {
|
||||
normal = Vec3::new(0.0, 0.0, if ray.direction.z < 0.0 { 1.0 } else { -1.0 });
|
||||
}
|
||||
|
||||
Some((t_enter, t_exit, normal))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if a position is within the current octree bounds.
|
||||
pub fn contains(&self, x: f64, y: f64, z: f64) -> bool {
|
||||
let half_size = self.size / 2.0;
|
||||
let epsilon = 1e-6; // Epsilon for floating-point precision
|
||||
|
||||
(x >= -half_size - epsilon && x < half_size + epsilon) &&
|
||||
(y >= -half_size - epsilon && y < half_size + epsilon) &&
|
||||
(z >= -half_size - epsilon && z < half_size + epsilon)
|
||||
}
|
||||
|
||||
pub fn get_voxel_at_world_coords(&self, world_x: f64, world_y: f64, world_z: f64) -> Option<&Voxel> {
|
||||
// Correct normalization: calculate the position relative to the octree's center
|
||||
let normalized_x = (world_x + (self.size / 2.0)) / self.size;
|
||||
let normalized_y = (world_y + (self.size / 2.0)) / self.size;
|
||||
let normalized_z = (world_z + (self.size / 2.0)) / self.size;
|
||||
|
||||
self.get_voxel_at(normalized_x, normalized_y, normalized_z)
|
||||
}
|
||||
|
||||
/// A small helper to compute chunk coords from a voxel's "true" world position
|
||||
pub fn compute_chunk_coords(&self, world_x: f64, world_y: f64, world_z: f64) -> (i64, i64, i64) {
|
||||
// The size of one voxel at max_depth
|
||||
let step = self.get_spacing_at_depth(self.max_depth);
|
||||
|
||||
// Each chunk is 16 voxels => chunk_size_world = 16.0 * step
|
||||
|
||||
let chunk_size = self.get_chunk_size();
|
||||
|
||||
|
||||
let chunk_size_world = chunk_size as f64 * step;
|
||||
|
||||
// Divide the world coords by chunk_size_world, floor => chunk coordinate
|
||||
let cx = (world_x / chunk_size_world).floor();
|
||||
let cy = (world_y / chunk_size_world).floor();
|
||||
let cz = (world_z / chunk_size_world).floor();
|
||||
|
||||
(cx as i64, cy as i64, cz as i64)
|
||||
}
|
||||
|
||||
pub fn has_volume(&self, node: &OctreeNode) -> bool {
|
||||
// Check if this node is a leaf with a voxel
|
||||
if node.is_leaf && node.voxel.is_some() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the node has children, recursively check them
|
||||
if let Some(children) = &node.children {
|
||||
for child in children.iter() {
|
||||
if self.has_volume(child) {
|
||||
return true; // If any child has a voxel, the chunk has volume
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no voxel found in this node or its children
|
||||
false
|
||||
}
|
||||
|
||||
pub fn get_chunk_size(&self) -> u32 {
|
||||
self.max_depth - 1
|
||||
}
|
||||
|
||||
pub fn get_chunk_node(&self, world_x: f64, world_y: f64, world_z: f64) -> Option<&OctreeNode> {
|
||||
|
||||
// Ensure the world position is within the octree's bounds
|
||||
if !self.contains(world_x, world_y, world_z) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Normalize the world position to the octree's space
|
||||
let normalized_x = (world_x + (self.size / 2.0)) / self.size;
|
||||
let normalized_y = (world_y + (self.size / 2.0)) / self.size;
|
||||
let normalized_z = (world_z + (self.size / 2.0)) / self.size;
|
||||
|
||||
|
||||
let chunk_size = self.get_chunk_size();
|
||||
|
||||
// Traverse to the appropriate chunk node
|
||||
Self::get_node_at_depth(&self.root, normalized_x, normalized_y, normalized_z, chunk_size)
|
||||
}
|
||||
|
||||
/// Helper function to recursively traverse the octree to a specific depth.
|
||||
fn get_node_at_depth(
|
||||
node: &OctreeNode,
|
||||
x: f64,
|
||||
y: f64,
|
||||
z: f64,
|
||||
depth: u32,
|
||||
) -> Option<&OctreeNode> {
|
||||
if depth == 0 {
|
||||
return Some(node); // We've reached the desired depth
|
||||
}
|
||||
|
||||
if let Some(ref children) = node.children {
|
||||
// Determine which child to traverse into
|
||||
let epsilon = 1e-6;
|
||||
let index = ((x >= 0.5 - epsilon) as usize)
|
||||
+ ((y >= 0.5 - epsilon) as usize * 2)
|
||||
+ ((z >= 0.5 - epsilon) as usize * 4);
|
||||
|
||||
let adjust_coord = |coord: f64| {
|
||||
if coord >= 0.5 - epsilon {
|
||||
(coord - 0.5) * 2.0
|
||||
} else {
|
||||
coord * 2.0
|
||||
}
|
||||
};
|
||||
|
||||
// Recurse into the correct child
|
||||
Self::get_node_at_depth(
|
||||
&children[index],
|
||||
adjust_coord(x),
|
||||
adjust_coord(y),
|
||||
adjust_coord(z),
|
||||
depth - 1,
|
||||
)
|
||||
} else {
|
||||
None // Node has no children at this depth
|
||||
}
|
||||
}
|
||||
|
||||
pub fn traverse_chunk(
|
||||
&self,
|
||||
node: &OctreeNode,
|
||||
chunk_size: u32,
|
||||
) -> Vec<(f32, f32, f32, Color, u32)> {
|
||||
let mut voxels = Vec::new();
|
||||
Self::traverse_chunk_recursive(node, 0.0, 0.0, 0.0, chunk_size as f32, 0, &mut voxels);
|
||||
voxels
|
||||
}
|
||||
|
||||
fn traverse_chunk_recursive(
|
||||
node: &OctreeNode,
|
||||
x: f32,
|
||||
y: f32,
|
||||
z: f32,
|
||||
size: f32,
|
||||
depth: u32,
|
||||
voxels: &mut Vec<(f32, f32, f32, Color, u32)>,
|
||||
) {
|
||||
if node.is_leaf {
|
||||
if let Some(voxel) = node.voxel {
|
||||
voxels.push((x, y, z, voxel.color, depth));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref children) = node.children {
|
||||
let half_size = size / 2.0;
|
||||
for (i, child) in children.iter().enumerate() {
|
||||
let offset = |bit: usize| if (i & bit) == bit { half_size } else { 0.0 };
|
||||
Self::traverse_chunk_recursive(
|
||||
child,
|
||||
x + offset(1),
|
||||
y + offset(2),
|
||||
z + offset(4),
|
||||
half_size,
|
||||
depth + 1,
|
||||
voxels,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Returns the (face_normal, local_offset) for the given neighbor direction.
|
||||
/// - `dx, dy, dz`: The integer direction of the face (-1,0,0 / 1,0,0 / etc.)
|
||||
/// - `voxel_size_f`: The world size of a single voxel (e.g. step as f32).
|
||||
pub fn face_orientation(dx: f64, dy: f64, dz: f64, voxel_size_f: f32) -> (Vec3, Vec3) {
|
||||
// We'll do a match on the direction
|
||||
match (dx, dy, dz) {
|
||||
// Negative X => face normal is (-1, 0, 0), local offset is -voxel_size/2 in X
|
||||
(-1.0, 0.0, 0.0) => {
|
||||
let normal = Vec3::new(-1.0, 0.0, 0.0);
|
||||
let offset = Vec3::new(-voxel_size_f * 0.5, 0.0, 0.0);
|
||||
(normal, offset)
|
||||
}
|
||||
// Positive X
|
||||
(1.0, 0.0, 0.0) => {
|
||||
let normal = Vec3::new(1.0, 0.0, 0.0);
|
||||
let offset = Vec3::new(voxel_size_f * 0.5, 0.0, 0.0);
|
||||
(normal, offset)
|
||||
}
|
||||
// Negative Y
|
||||
(0.0, -1.0, 0.0) => {
|
||||
let normal = Vec3::new(0.0, -1.0, 0.0);
|
||||
let offset = Vec3::new(0.0, -voxel_size_f * 0.5, 0.0);
|
||||
(normal, offset)
|
||||
}
|
||||
// Positive Y
|
||||
(0.0, 1.0, 0.0) => {
|
||||
let normal = Vec3::new(0.0, 1.0, 0.0);
|
||||
let offset = Vec3::new(0.0, voxel_size_f * 0.5, 0.0);
|
||||
(normal, offset)
|
||||
}
|
||||
// Negative Z
|
||||
(0.0, 0.0, -1.0) => {
|
||||
let normal = Vec3::new(0.0, 0.0, -1.0);
|
||||
let offset = Vec3::new(0.0, 0.0, -voxel_size_f * 0.5);
|
||||
(normal, offset)
|
||||
}
|
||||
// Positive Z
|
||||
(0.0, 0.0, 1.0) => {
|
||||
let normal = Vec3::new(0.0, 0.0, 1.0);
|
||||
let offset = Vec3::new(0.0, 0.0, voxel_size_f * 0.5);
|
||||
(normal, offset)
|
||||
}
|
||||
// If the direction is not one of the 6 axis directions, you might skip or handle differently
|
||||
_ => {
|
||||
// For safety, we can panic or return a default.
|
||||
// But typically you won't call face_orientation with an invalid direction
|
||||
panic!("Invalid face direction: ({}, {}, {})", dx, dy, dz);
|
||||
}
|
||||
}
|
||||
}
|
||||
5
src/systems/voxels/mod.rs
Normal file
5
src/systems/voxels/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
pub mod debug;
|
||||
pub mod helper;
|
||||
pub mod octree;
|
||||
pub mod structure;
|
||||
pub mod rendering;
|
||||
401
src/systems/voxels/octree.rs
Normal file
401
src/systems/voxels/octree.rs
Normal file
@ -0,0 +1,401 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use bevy::asset::Assets;
|
||||
use bevy::color::Color;
|
||||
use bevy::math::{DQuat, DVec3};
|
||||
use bevy::prelude::*;
|
||||
use bevy::render::mesh::{Indices, PrimitiveTopology, VertexAttributeValues};
|
||||
use bevy::render::render_asset::RenderAssetUsages;
|
||||
use crate::helper::large_transform::DoubleTransform;
|
||||
use crate::systems::voxels::structure::{ChunkEntities, OctreeNode, Ray, SparseVoxelOctree, Voxel, AABB, CHUNK_BUILD_BUDGET, CHUNK_NEIGHBOR_OFFSETS, CHUNK_RENDER_DISTANCE, NEIGHBOR_OFFSETS};
|
||||
|
||||
impl SparseVoxelOctree {
|
||||
/// Creates a new octree with the specified max depth, size, and wireframe visibility.
|
||||
pub fn new(max_depth: u32, size: f64, show_wireframe: bool, show_world_grid: bool, show_chunks: bool) -> Self {
|
||||
Self {
|
||||
root: OctreeNode::new(),
|
||||
max_depth,
|
||||
size,
|
||||
show_wireframe,
|
||||
show_world_grid,
|
||||
show_chunks,
|
||||
dirty_chunks: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, world_x: f64, world_y: f64, world_z: f64, voxel: Voxel) {
|
||||
// Normalize the world coordinates to the nearest voxel grid position
|
||||
let (aligned_x, aligned_y, aligned_z) = self.normalize_to_voxel_at_depth(world_x, world_y, world_z, self.max_depth);
|
||||
|
||||
// Iteratively expand the root to include the voxel position
|
||||
while !self.contains(aligned_x, aligned_y, aligned_z) {
|
||||
self.expand_root(aligned_x, aligned_y, aligned_z);
|
||||
}
|
||||
|
||||
// Correct normalization: calculate the position relative to the octree's center
|
||||
let normalized_x = (aligned_x + (self.size / 2.0)) / self.size;
|
||||
let normalized_y = (aligned_y + (self.size / 2.0)) / self.size;
|
||||
let normalized_z = (aligned_z + (self.size / 2.0)) / self.size;
|
||||
|
||||
// Insert the voxel with its world position
|
||||
let mut voxel_with_position = voxel;
|
||||
voxel_with_position.position = Vec3::new(world_x as f32, world_y as f32, world_z as f32);
|
||||
|
||||
|
||||
// Actually let's do a small helper:
|
||||
let (cx, cy, cz) = self.compute_chunk_coords(world_x, world_y, world_z);
|
||||
self.dirty_chunks.insert((cx as i32, cy as i32, cz as i32));
|
||||
|
||||
// 5b) Also mark the 6 neighboring chunks dirty to fix boundary faces
|
||||
for &(nx, ny, nz) in CHUNK_NEIGHBOR_OFFSETS.iter() {
|
||||
let neighbor_coord = (cx as i32 + nx, cy as i32 + ny, cz as i32 + nz);
|
||||
self.dirty_chunks.insert(neighbor_coord);
|
||||
}
|
||||
|
||||
|
||||
SparseVoxelOctree::insert_recursive(&mut self.root, normalized_x, normalized_y, normalized_z, voxel_with_position, self.max_depth);
|
||||
}
|
||||
|
||||
fn insert_recursive(node: &mut OctreeNode, x: f64, y: f64, z: f64, voxel: Voxel, depth: u32) {
|
||||
if depth == 0 {
|
||||
node.voxel = Some(voxel);
|
||||
node.is_leaf = true;
|
||||
return;
|
||||
}
|
||||
|
||||
let epsilon = 1e-6; // Epsilon for floating-point precision
|
||||
|
||||
let index = ((x >= 0.5 - epsilon) as usize) + ((y >= 0.5 - epsilon) as usize * 2) + ((z >= 0.5 - epsilon) as usize * 4);
|
||||
|
||||
if node.children.is_none() {
|
||||
node.children = Some(Box::new(core::array::from_fn(|_| OctreeNode::new())));
|
||||
node.is_leaf = false;
|
||||
}
|
||||
|
||||
if let Some(ref mut children) = node.children {
|
||||
let adjust_coord = |coord: f64| {
|
||||
if coord >= 0.5 - epsilon {
|
||||
(coord - 0.5) * 2.0
|
||||
} else {
|
||||
coord * 2.0
|
||||
}
|
||||
};
|
||||
SparseVoxelOctree::insert_recursive(&mut children[index], adjust_coord(x), adjust_coord(y), adjust_coord(z), voxel, depth - 1);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, world_x: f64, world_y: f64, world_z: f64) {
|
||||
// Normalize the world coordinates to the nearest voxel grid position
|
||||
let (aligned_x, aligned_y, aligned_z) =
|
||||
self.normalize_to_voxel_at_depth(world_x, world_y, world_z, self.max_depth);
|
||||
|
||||
// Correct normalization: calculate the position relative to the octree's center
|
||||
let normalized_x = (aligned_x + (self.size / 2.0)) / self.size;
|
||||
let normalized_y = (aligned_y + (self.size / 2.0)) / self.size;
|
||||
let normalized_z = (aligned_z + (self.size / 2.0)) / self.size;
|
||||
|
||||
// figure out chunk coords for the removed voxel:
|
||||
let (cx, cy, cz) = self.compute_chunk_coords(world_x, world_y, world_z);
|
||||
self.dirty_chunks.insert((cx as i32, cy as i32, cz as i32));
|
||||
|
||||
for &(nx, ny, nz) in CHUNK_NEIGHBOR_OFFSETS.iter() {
|
||||
let neighbor_coord = (cx as i32 + nx, cy as i32 + ny, cz as i32 + nz);
|
||||
self.dirty_chunks.insert(neighbor_coord);
|
||||
}
|
||||
|
||||
|
||||
// Call the recursive remove function
|
||||
Self::remove_recursive(&mut self.root, normalized_x, normalized_y, normalized_z, self.max_depth);
|
||||
}
|
||||
|
||||
fn remove_recursive(node: &mut OctreeNode, x: f64, y: f64, z: f64, depth: u32) -> bool {
|
||||
if depth == 0 {
|
||||
// This is the leaf node where the voxel should be
|
||||
if node.voxel.is_some() {
|
||||
node.voxel = None;
|
||||
node.is_leaf = false;
|
||||
// Since we've removed the voxel and there are no children, this node can be pruned
|
||||
return true;
|
||||
} else {
|
||||
// There was no voxel here
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if node.children.is_none() {
|
||||
// No children to traverse, voxel not found
|
||||
return false;
|
||||
}
|
||||
|
||||
let epsilon = 1e-6; // Epsilon for floating-point precision
|
||||
let index = ((x >= 0.5 - epsilon) as usize)
|
||||
+ ((y >= 0.5 - epsilon) as usize * 2)
|
||||
+ ((z >= 0.5 - epsilon) as usize * 4);
|
||||
|
||||
let adjust_coord = |coord: f64| {
|
||||
if coord >= 0.5 - epsilon {
|
||||
(coord - 0.5) * 2.0
|
||||
} else {
|
||||
coord * 2.0
|
||||
}
|
||||
};
|
||||
|
||||
let child = &mut node.children.as_mut().unwrap()[index];
|
||||
|
||||
let should_prune_child = Self::remove_recursive(
|
||||
child,
|
||||
adjust_coord(x),
|
||||
adjust_coord(y),
|
||||
adjust_coord(z),
|
||||
depth - 1,
|
||||
);
|
||||
|
||||
if should_prune_child {
|
||||
// Remove the child node
|
||||
node.children.as_mut().unwrap()[index] = OctreeNode::new();
|
||||
}
|
||||
|
||||
// After removing the child, check if all children are empty
|
||||
let all_children_empty = node.children.as_ref().unwrap().iter().all(|child| child.is_empty());
|
||||
|
||||
if all_children_empty {
|
||||
// Remove the children array
|
||||
node.children = None;
|
||||
node.is_leaf = true; // Now this node becomes a leaf
|
||||
// If this node has no voxel and no children, it can be pruned
|
||||
return node.voxel.is_none();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn expand_root(&mut self, x: f64, y: f64, z: f64) {
|
||||
let new_size = self.size * 2.0;
|
||||
let new_depth = self.max_depth + 1;
|
||||
|
||||
// Create a new root node with 8 children
|
||||
let mut new_root = OctreeNode::new();
|
||||
new_root.children = Some(Box::new(core::array::from_fn(|_| OctreeNode::new())));
|
||||
|
||||
// The old root had 8 children; move each child to the correct new position
|
||||
if let Some(old_children) = self.root.children.take() {
|
||||
for (i, old_child) in old_children.iter().enumerate() {
|
||||
// Determine which child of the new root the old child belongs in
|
||||
let offset_x = if (i & 1) == 1 { 1 } else { 0 };
|
||||
let offset_y = if (i & 2) == 2 { 1 } else { 0 };
|
||||
let offset_z = if (i & 4) == 4 { 1 } else { 0 };
|
||||
|
||||
let new_index = offset_x + (offset_y * 2) + (offset_z * 4);
|
||||
|
||||
// Now, move the old child into the correct new child of the new root
|
||||
let new_child = &mut new_root.children.as_mut().unwrap()[new_index];
|
||||
|
||||
// Create new children for the new child if necessary
|
||||
if new_child.children.is_none() {
|
||||
new_child.children = Some(Box::new(core::array::from_fn(|_| OctreeNode::new())));
|
||||
}
|
||||
|
||||
// Place the old child in the correct "facing" position in the new child
|
||||
let facing_x = if offset_x == 1 { 0 } else { 1 };
|
||||
let facing_y = if offset_y == 1 { 0 } else { 1 };
|
||||
let facing_z = if offset_z == 1 { 0 } else { 1 };
|
||||
|
||||
let facing_index = facing_x + (facing_y * 2) + (facing_z * 4);
|
||||
new_child.children.as_mut().unwrap()[facing_index] = old_child.clone();
|
||||
}
|
||||
}
|
||||
|
||||
self.root = new_root;
|
||||
self.size = new_size;
|
||||
self.max_depth = new_depth;
|
||||
}
|
||||
|
||||
|
||||
/// Traverse the octree and collect voxel data.
|
||||
pub fn traverse(&self) -> Vec<(f32, f32, f32, Color, u32)> {
|
||||
let mut voxels = Vec::new();
|
||||
Self::traverse_recursive(&self.root, 0.0, 0.0, 0.0, 1.0, 0, &mut voxels);
|
||||
voxels
|
||||
}
|
||||
|
||||
fn traverse_recursive(
|
||||
node: &OctreeNode,
|
||||
x: f32,
|
||||
y: f32,
|
||||
z: f32,
|
||||
size: f32,
|
||||
depth: u32,
|
||||
voxels: &mut Vec<(f32, f32, f32, Color, u32)>,
|
||||
) {
|
||||
if node.is_leaf/* && !node.is_constant*/ {
|
||||
if let Some(voxel) = node.voxel {
|
||||
voxels.push((x, y, z, voxel.color, depth));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref children) = node.children {
|
||||
let half_size = size / 2.0;
|
||||
for (i, child) in children.iter().enumerate() {
|
||||
let offset = |bit: usize| if (i & bit) == bit { half_size } else { 0.0 };
|
||||
Self::traverse_recursive(
|
||||
child,
|
||||
x + offset(1),
|
||||
y + offset(2),
|
||||
z + offset(4),
|
||||
half_size,
|
||||
depth + 1,
|
||||
voxels,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Retrieves a reference to the voxel at the given normalized coordinates and depth, if it exists.
|
||||
pub fn get_voxel_at(&self, x: f64, y: f64, z: f64) -> Option<&Voxel> {
|
||||
Self::get_voxel_recursive(&self.root, x, y, z)
|
||||
}
|
||||
|
||||
fn get_voxel_recursive(
|
||||
node: &OctreeNode,
|
||||
x: f64,
|
||||
y: f64,
|
||||
z: f64,
|
||||
) -> Option<&Voxel> {
|
||||
if node.is_leaf {
|
||||
return node.voxel.as_ref();
|
||||
}
|
||||
|
||||
if let Some(ref children) = node.children {
|
||||
let epsilon = 1e-6; // Epsilon for floating-point precision
|
||||
let index = ((x >= 0.5 - epsilon) as usize)
|
||||
+ ((y >= 0.5 - epsilon) as usize * 2)
|
||||
+ ((z >= 0.5 - epsilon) as usize * 4);
|
||||
|
||||
let adjust_coord = |coord: f64| {
|
||||
if coord >= 0.5 - epsilon {
|
||||
(coord - 0.5) * 2.0
|
||||
} else {
|
||||
coord * 2.0
|
||||
}
|
||||
};
|
||||
|
||||
Self::get_voxel_recursive(
|
||||
&children[index],
|
||||
adjust_coord(x),
|
||||
adjust_coord(y),
|
||||
adjust_coord(z),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if there is a neighbor voxel at the specified direction from the given world coordinates at the specified depth.
|
||||
/// The offsets are directions (-1, 0, 1) for x, y, z.
|
||||
pub fn has_neighbor(
|
||||
&self,
|
||||
world_x: f64,
|
||||
world_y: f64,
|
||||
world_z: f64,
|
||||
offset_x: i32,
|
||||
offset_y: i32,
|
||||
offset_z: i32,
|
||||
depth: u32,
|
||||
) -> bool {
|
||||
// Normalize the world coordinates to the nearest voxel grid position at the specified depth
|
||||
let (aligned_x, aligned_y, aligned_z) =
|
||||
self.normalize_to_voxel_at_depth(world_x, world_y, world_z, depth);
|
||||
|
||||
// Calculate the voxel size at the specified depth
|
||||
let voxel_size = self.get_spacing_at_depth(depth);
|
||||
|
||||
// Calculate the neighbor's world position
|
||||
let neighbor_x = aligned_x + (offset_x as f64) * voxel_size;
|
||||
let neighbor_y = aligned_y + (offset_y as f64) * voxel_size;
|
||||
let neighbor_z = aligned_z + (offset_z as f64) * voxel_size;
|
||||
|
||||
// Check if the neighbor position is within bounds
|
||||
if !self.contains(neighbor_x, neighbor_y, neighbor_z) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the voxel in the neighboring position
|
||||
self.get_voxel_at_world_coords(neighbor_x, neighbor_y, neighbor_z)
|
||||
.is_some()
|
||||
}
|
||||
|
||||
|
||||
/// Performs a raycast against the octree and returns the first intersected voxel.
|
||||
pub fn raycast(&self, ray: &Ray) -> Option<(f64, f64, f64, u32, Vec3)> {
|
||||
// Start from the root node
|
||||
let half_size = self.size / 2.0;
|
||||
let root_bounds = AABB {
|
||||
min: Vec3::new(-half_size as f32, -half_size as f32, -half_size as f32),
|
||||
max: Vec3::new(half_size as f32, half_size as f32, half_size as f32),
|
||||
};
|
||||
self.raycast_recursive(
|
||||
&self.root,
|
||||
ray,
|
||||
&root_bounds,
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
fn raycast_recursive(
|
||||
&self,
|
||||
node: &OctreeNode,
|
||||
ray: &Ray,
|
||||
bounds: &AABB,
|
||||
depth: u32,
|
||||
) -> Option<(f64, f64, f64, u32, Vec3)> {
|
||||
// Check if the ray intersects this node's bounding box
|
||||
if let Some((t_enter, _, normal)) = self.ray_intersects_aabb_with_normal(ray, bounds) {
|
||||
// If this is a leaf node and contains a voxel, return it
|
||||
if node.is_leaf && node.voxel.is_some() {
|
||||
// Compute the exact hit position
|
||||
let hit_position = ray.origin + ray.direction * t_enter;
|
||||
|
||||
// Return the hit position along with depth and normal
|
||||
return Some((
|
||||
hit_position.x as f64,
|
||||
hit_position.y as f64,
|
||||
hit_position.z as f64,
|
||||
depth,
|
||||
normal,
|
||||
));
|
||||
}
|
||||
|
||||
// If the node has children, traverse them
|
||||
if let Some(ref children) = node.children {
|
||||
// For each child, compute its bounding box and recurse
|
||||
let mut hits = Vec::new();
|
||||
for (i, child) in children.iter().enumerate() {
|
||||
let child_bounds = self.compute_child_bounds(bounds, i);
|
||||
if let Some(hit) = self.raycast_recursive(child, ray, &child_bounds, depth + 1) {
|
||||
hits.push(hit);
|
||||
}
|
||||
}
|
||||
// Return the closest hit, if any
|
||||
if !hits.is_empty() {
|
||||
hits.sort_by(|a, b| {
|
||||
let dist_a = ((a.0 as f32 - ray.origin.x).powi(2)
|
||||
+ (a.1 as f32 - ray.origin.y).powi(2)
|
||||
+ (a.2 as f32 - ray.origin.z).powi(2))
|
||||
.sqrt();
|
||||
let dist_b = ((b.0 as f32 - ray.origin.x).powi(2)
|
||||
+ (b.1 as f32 - ray.origin.y).powi(2)
|
||||
+ (b.2 as f32 - ray.origin.z).powi(2))
|
||||
.sqrt();
|
||||
dist_a.partial_cmp(&dist_b).unwrap()
|
||||
});
|
||||
return Some(hits[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
543
src/systems/voxels/rendering.rs
Normal file
543
src/systems/voxels/rendering.rs
Normal file
@ -0,0 +1,543 @@
|
||||
// Chunk Rendering
|
||||
|
||||
use bevy::math::{DQuat, DVec3};
|
||||
use bevy::prelude::*;
|
||||
use bevy::utils::info;
|
||||
use bevy_asset::RenderAssetUsages;
|
||||
use bevy_render::mesh::{Indices, PrimitiveTopology, VertexAttributeValues};
|
||||
use crate::helper::large_transform::{DoubleTransform, WorldOffset};
|
||||
use crate::systems::voxels::structure::{ChunkEntities, ChunkMarker, SparseVoxelOctree, CHUNK_BUILD_BUDGET, CHUNK_RENDER_DISTANCE, NEIGHBOR_OFFSETS};
|
||||
use crate::helper::large_transform::get_true_world_position;
|
||||
use crate::systems::voxels::helper::face_orientation;
|
||||
|
||||
/*pub fn render(
|
||||
mut commands: Commands,
|
||||
mut query: Query<&mut SparseVoxelOctree>,
|
||||
mut octree_transform_query: Query<&DoubleTransform, With<SparseVoxelOctree>>,
|
||||
render_object_query: Query<Entity, With<VoxelTerrainMarker>>,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
camera_query: Query<&Transform, With<Camera>>,
|
||||
) {
|
||||
// Get the camera's current position (if needed for LOD calculations)
|
||||
let camera_transform = camera_query.single();
|
||||
let _camera_position = camera_transform.translation;
|
||||
|
||||
|
||||
for mut octree in query.iter_mut() {
|
||||
|
||||
|
||||
|
||||
// Handle updates to the octree only if it is marked as dirty
|
||||
if !octree.dirty_chunks.is_empty() {
|
||||
// Clear existing render objects
|
||||
for entity in render_object_query.iter() {
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
|
||||
// Collect the voxels to render
|
||||
let voxels = octree.traverse();
|
||||
|
||||
let mut voxel_meshes = Vec::new();
|
||||
|
||||
for (x, y, z, _color, depth) in voxels {
|
||||
let voxel_size = octree.get_spacing_at_depth(depth) as f32;
|
||||
|
||||
// Calculate the world position of the voxel
|
||||
let world_position = Vec3::new(
|
||||
(x * octree.size as f32) + (voxel_size / 2.0) - (octree.size / 2.0) as f32,
|
||||
(y * octree.size as f32) + (voxel_size / 2.0) - (octree.size / 2.0) as f32,
|
||||
(z * octree.size as f32) + (voxel_size / 2.0) - (octree.size / 2.0) as f32,
|
||||
);
|
||||
|
||||
// Convert world_position components to f64 for neighbor checking
|
||||
let world_x = world_position.x as f64;
|
||||
let world_y = world_position.y as f64;
|
||||
let world_z = world_position.z as f64;
|
||||
|
||||
// Iterate over all possible neighbor offsets
|
||||
for &(dx, dy, dz) in NEIGHBOR_OFFSETS.iter() {
|
||||
|
||||
// Check if there's no neighbor in this direction
|
||||
if !octree.has_neighbor(world_x, world_y, world_z, dx as i32, dy as i32, dz as i32, depth) {
|
||||
|
||||
// Determine the face normal and local position based on the direction
|
||||
let (normal, local_position) = match (dx, dy, dz) {
|
||||
(-1.0, 0.0, 0.0) => (
|
||||
Vec3::new(-1.0, 0.0, 0.0),
|
||||
Vec3::new(-voxel_size / 2.0, 0.0, 0.0),
|
||||
),
|
||||
(1.0, 0.0, 0.0) => (
|
||||
Vec3::new(1.0, 0.0, 0.0),
|
||||
Vec3::new(voxel_size / 2.0, 0.0, 0.0),
|
||||
),
|
||||
(0.0, -1.0, 0.0) => (
|
||||
Vec3::new(0.0, -1.0, 0.0),
|
||||
Vec3::new(0.0, -voxel_size / 2.0, 0.0),
|
||||
),
|
||||
(0.0, 1.0, 0.0) => (
|
||||
Vec3::new(0.0, 1.0, 0.0),
|
||||
Vec3::new(0.0, voxel_size / 2.0, 0.0),
|
||||
),
|
||||
(0.0, 0.0, -1.0) => (
|
||||
Vec3::new(0.0, 0.0, -1.0),
|
||||
Vec3::new(0.0, 0.0, -voxel_size / 2.0),
|
||||
),
|
||||
(0.0, 0.0, 1.0) => (
|
||||
Vec3::new(0.0, 0.0, 1.0),
|
||||
Vec3::new(0.0, 0.0, voxel_size / 2.0),
|
||||
),
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
// Generate the face for rendering
|
||||
voxel_meshes.push(generate_face(
|
||||
normal,
|
||||
local_position,
|
||||
world_position,
|
||||
voxel_size / 2.0,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge the voxel meshes into a single mesh
|
||||
let mesh = merge_meshes(voxel_meshes);
|
||||
let cube_handle = meshes.add(mesh);
|
||||
|
||||
// Spawn the mesh into the scene
|
||||
commands.spawn((
|
||||
PbrBundle {
|
||||
mesh: Mesh3d::from(cube_handle),
|
||||
material: MeshMaterial3d::from(materials.add(StandardMaterial {
|
||||
base_color: Color::srgb(0.8, 0.7, 0.6),
|
||||
..Default::default()
|
||||
})),
|
||||
transform: Default::default(),
|
||||
..Default::default()
|
||||
},
|
||||
VoxelTerrainMarker {},
|
||||
DoubleTransform {
|
||||
translation: octree_transform_query.single().translation,
|
||||
rotation: DQuat::IDENTITY,
|
||||
scale: DVec3::ONE,
|
||||
},
|
||||
));
|
||||
|
||||
// Reset the dirty flag once the update is complete
|
||||
octree.dirty_chunks.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct VoxelTerrainMarker;
|
||||
|
||||
|
||||
pub fn render(
|
||||
mut commands: Commands,
|
||||
mut octree_query: Query<&mut SparseVoxelOctree>,
|
||||
octree_transform_query: Query<&DoubleTransform, With<SparseVoxelOctree>>,
|
||||
mut chunk_entities: ResMut<ChunkEntities>,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
// Use DoubleTransform for the camera
|
||||
camera_query: Query<&DoubleTransform, With<Camera>>,
|
||||
) {
|
||||
let mut octree = match octree_query.get_single_mut() {
|
||||
Ok(o) => o,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let camera_dt = match camera_query.get_single() {
|
||||
Ok(dt) => dt,
|
||||
Err(_) => return,
|
||||
};
|
||||
// Convert camera's double position to f32 for distance calculations.
|
||||
let camera_pos = camera_dt.translation.as_vec3();
|
||||
|
||||
let octree_dt = octree_transform_query.single();
|
||||
let octree_offset = octree_dt.translation.as_vec3();
|
||||
|
||||
// Define chunk sizing.
|
||||
let step = octree.get_spacing_at_depth(octree.max_depth);
|
||||
let chunk_world_size = octree.get_chunk_size() as f32 * step as f32;
|
||||
|
||||
// 1) DESPAWN out-of-range chunks.
|
||||
let mut chunks_to_remove = Vec::new();
|
||||
for (&(cx, cy, cz), &entity) in chunk_entities.map.iter() {
|
||||
let chunk_min = Vec3::new(
|
||||
cx as f32 * chunk_world_size,
|
||||
cy as f32 * chunk_world_size,
|
||||
cz as f32 * chunk_world_size,
|
||||
);
|
||||
let chunk_center = chunk_min + Vec3::splat(chunk_world_size * 0.5);
|
||||
let final_center = octree_offset + chunk_center;
|
||||
let dist = camera_pos.distance(final_center);
|
||||
if dist > CHUNK_RENDER_DISTANCE as f32 {
|
||||
chunks_to_remove.push((cx, cy, cz, entity));
|
||||
}
|
||||
}
|
||||
for (cx, cy, cz, e) in chunks_to_remove {
|
||||
commands.entity(e).despawn();
|
||||
chunk_entities.map.remove(&(cx, cy, cz));
|
||||
}
|
||||
|
||||
// 2) LOAD new in-range chunks with nearest-first ordering.
|
||||
let camera_cx = ((camera_pos.x - octree_offset.x) / chunk_world_size).floor() as i32;
|
||||
let camera_cy = ((camera_pos.y - octree_offset.y) / chunk_world_size).floor() as i32;
|
||||
let camera_cz = ((camera_pos.z - octree_offset.z) / chunk_world_size).floor() as i32;
|
||||
|
||||
let half_chunks = (CHUNK_RENDER_DISTANCE / chunk_world_size as f64).ceil() as i32;
|
||||
let mut new_chunks_to_spawn = Vec::new();
|
||||
for dx in -half_chunks..=half_chunks {
|
||||
for dy in -half_chunks..=half_chunks {
|
||||
for dz in -half_chunks..=half_chunks {
|
||||
let cc = (camera_cx + dx, camera_cy + dy, camera_cz + dz);
|
||||
if !chunk_entities.map.contains_key(&cc) {
|
||||
let chunk_min = Vec3::new(
|
||||
cc.0 as f32 * chunk_world_size,
|
||||
cc.1 as f32 * chunk_world_size,
|
||||
cc.2 as f32 * chunk_world_size,
|
||||
);
|
||||
let chunk_center = chunk_min + Vec3::splat(chunk_world_size * 0.5);
|
||||
let final_center = octree_offset + chunk_center;
|
||||
let dist = camera_pos.distance(final_center);
|
||||
if dist <= CHUNK_RENDER_DISTANCE as f32 {
|
||||
new_chunks_to_spawn.push(cc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Sort candidate chunks by distance (nearest first).
|
||||
new_chunks_to_spawn.sort_by(|a, b| {
|
||||
let pos_a = octree_offset
|
||||
+ Vec3::new(
|
||||
a.0 as f32 * chunk_world_size,
|
||||
a.1 as f32 * chunk_world_size,
|
||||
a.2 as f32 * chunk_world_size,
|
||||
)
|
||||
+ Vec3::splat(chunk_world_size * 0.5);
|
||||
let pos_b = octree_offset
|
||||
+ Vec3::new(
|
||||
b.0 as f32 * chunk_world_size,
|
||||
b.1 as f32 * chunk_world_size,
|
||||
b.2 as f32 * chunk_world_size,
|
||||
)
|
||||
+ Vec3::splat(chunk_world_size * 0.5);
|
||||
camera_pos
|
||||
.distance(pos_a)
|
||||
.partial_cmp(&camera_pos.distance(pos_b))
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
let build_budget = 5; // Maximum chunks to build per frame.
|
||||
let mut spawn_count = 0;
|
||||
for cc in new_chunks_to_spawn {
|
||||
if spawn_count >= build_budget {
|
||||
break;
|
||||
}
|
||||
// Compute chunk's world position.
|
||||
let chunk_min = Vec3::new(
|
||||
cc.0 as f32 * chunk_world_size,
|
||||
cc.1 as f32 * chunk_world_size,
|
||||
cc.2 as f32 * chunk_world_size,
|
||||
);
|
||||
let chunk_center = chunk_min + Vec3::splat(chunk_world_size * 0.5);
|
||||
// Check if this chunk has any voxels.
|
||||
if let Some(chunk_node) =
|
||||
octree.get_chunk_node(chunk_center.x as f64, chunk_center.y as f64, chunk_center.z as f64)
|
||||
{
|
||||
if octree.has_volume(chunk_node) {
|
||||
info!("Loading chunk at: {},{},{} (has volume)", cc.0, cc.1, cc.2);
|
||||
}
|
||||
}
|
||||
build_and_spawn_chunk(
|
||||
&mut commands,
|
||||
&octree,
|
||||
&mut meshes,
|
||||
&mut materials,
|
||||
&mut chunk_entities,
|
||||
cc,
|
||||
octree_offset,
|
||||
);
|
||||
spawn_count += 1;
|
||||
}
|
||||
|
||||
// 3) Rebuild dirty chunks (if any) with nearest-first ordering and budget.
|
||||
if !octree.dirty_chunks.is_empty() {
|
||||
let mut dirty = octree.dirty_chunks.drain().collect::<Vec<_>>();
|
||||
dirty.sort_by(|a, b| {
|
||||
let pos_a = octree_offset
|
||||
+ Vec3::new(
|
||||
a.0 as f32 * chunk_world_size,
|
||||
a.1 as f32 * chunk_world_size,
|
||||
a.2 as f32 * chunk_world_size,
|
||||
)
|
||||
+ Vec3::splat(chunk_world_size * 0.5);
|
||||
let pos_b = octree_offset
|
||||
+ Vec3::new(
|
||||
b.0 as f32 * chunk_world_size,
|
||||
b.1 as f32 * chunk_world_size,
|
||||
b.2 as f32 * chunk_world_size,
|
||||
)
|
||||
+ Vec3::splat(chunk_world_size * 0.5);
|
||||
camera_pos
|
||||
.distance(pos_a)
|
||||
.partial_cmp(&camera_pos.distance(pos_b))
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
let mut rebuild_count = 0;
|
||||
for chunk_coord in dirty {
|
||||
if rebuild_count >= build_budget {
|
||||
octree.dirty_chunks.insert(chunk_coord);
|
||||
continue;
|
||||
}
|
||||
let chunk_min = Vec3::new(
|
||||
chunk_coord.0 as f32 * chunk_world_size,
|
||||
chunk_coord.1 as f32 * chunk_world_size,
|
||||
chunk_coord.2 as f32 * chunk_world_size,
|
||||
);
|
||||
let chunk_center = chunk_min + Vec3::splat(chunk_world_size * 0.5);
|
||||
let final_center = octree_offset + chunk_center;
|
||||
let dist = camera_pos.distance(final_center);
|
||||
|
||||
if dist <= CHUNK_RENDER_DISTANCE as f32 {
|
||||
if let Some(chunk_node) =
|
||||
octree.get_chunk_node(chunk_center.x as f64, chunk_center.y as f64, chunk_center.z as f64)
|
||||
{
|
||||
if octree.has_volume(chunk_node) {
|
||||
info!(
|
||||
"Rebuilding chunk at: {},{},{} (has volume)",
|
||||
chunk_coord.0, chunk_coord.1, chunk_coord.2
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(e) = chunk_entities.map.remove(&chunk_coord) {
|
||||
commands.entity(e).despawn();
|
||||
}
|
||||
build_and_spawn_chunk(
|
||||
&mut commands,
|
||||
&octree,
|
||||
&mut meshes,
|
||||
&mut materials,
|
||||
&mut chunk_entities,
|
||||
chunk_coord,
|
||||
octree_offset,
|
||||
);
|
||||
rebuild_count += 1;
|
||||
} else {
|
||||
if let Some(e) = chunk_entities.map.remove(&chunk_coord) {
|
||||
commands.entity(e).despawn();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
fn build_and_spawn_chunk(
|
||||
commands: &mut Commands,
|
||||
octree: &SparseVoxelOctree,
|
||||
meshes: &mut ResMut<Assets<Mesh>>,
|
||||
materials: &mut ResMut<Assets<StandardMaterial>>,
|
||||
chunk_entities: &mut ChunkEntities,
|
||||
chunk_coord: (i32, i32, i32),
|
||||
octree_offset: Vec3,
|
||||
) {
|
||||
let face_meshes = build_chunk_geometry(octree, chunk_coord);
|
||||
if face_meshes.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let merged = merge_meshes(face_meshes);
|
||||
let mesh_handle = meshes.add(merged);
|
||||
|
||||
let step = octree.get_spacing_at_depth(octree.max_depth);
|
||||
let chunk_world_size = octree.get_chunk_size() as f64 * step;
|
||||
let chunk_min = Vec3::new(
|
||||
chunk_coord.0 as f32 * chunk_world_size as f32,
|
||||
chunk_coord.1 as f32 * chunk_world_size as f32,
|
||||
chunk_coord.2 as f32 * chunk_world_size as f32,
|
||||
);
|
||||
let final_pos = octree_offset + chunk_min;
|
||||
|
||||
let e = commands.spawn((
|
||||
PbrBundle {
|
||||
mesh: mesh_handle.into(),
|
||||
material: materials.add(StandardMaterial {
|
||||
base_color: Color::rgb(0.8, 0.7, 0.6),
|
||||
..default()
|
||||
}).into(),
|
||||
transform: Transform::from_translation(final_pos),
|
||||
..default()
|
||||
},
|
||||
VoxelTerrainMarker,
|
||||
DoubleTransform {
|
||||
translation: DVec3::from(final_pos),
|
||||
rotation: DQuat::IDENTITY,
|
||||
scale: DVec3::ONE,
|
||||
},
|
||||
))
|
||||
.id();
|
||||
|
||||
chunk_entities.map.insert(chunk_coord, e);
|
||||
}
|
||||
|
||||
fn build_chunk_geometry(
|
||||
octree: &SparseVoxelOctree,
|
||||
(cx, cy, cz): (i32, i32, i32),
|
||||
) -> Vec<Mesh> {
|
||||
let mut face_meshes = Vec::new();
|
||||
|
||||
// step in world units for one voxel at max_depth
|
||||
let step = octree.get_spacing_at_depth(octree.max_depth);
|
||||
let chunk_size = octree.get_chunk_size();
|
||||
|
||||
// chunk is 16 voxels => chunk_min in world space:
|
||||
let chunk_min_x = cx as f64 * (chunk_size as f64 * step);
|
||||
let chunk_min_y = cy as f64 * (chunk_size as f64 * step);
|
||||
let chunk_min_z = cz as f64 * (chunk_size as f64 * step);
|
||||
|
||||
// for local offset
|
||||
let chunk_min_f32 = Vec3::new(
|
||||
chunk_min_x as f32,
|
||||
chunk_min_y as f32,
|
||||
chunk_min_z as f32,
|
||||
);
|
||||
let voxel_size_f = step as f32;
|
||||
|
||||
// i in [0..16] => corner is chunk_min_x + i*step
|
||||
// no +0.5 => corners approach
|
||||
for i in 0..chunk_size {
|
||||
let vx = chunk_min_x + i as f64 * step;
|
||||
for j in 0..chunk_size {
|
||||
let vy = chunk_min_y + j as f64 * step;
|
||||
for k in 0..chunk_size {
|
||||
let vz = chunk_min_z + k as f64 * step;
|
||||
|
||||
// check if we have a voxel at that corner
|
||||
if let Some(_) = octree.get_voxel_at_world_coords(vx, vy, vz) {
|
||||
// check neighbors
|
||||
for &(dx, dy, dz) in NEIGHBOR_OFFSETS.iter() {
|
||||
let nx = vx + dx as f64 * step;
|
||||
let ny = vy + dy as f64 * step;
|
||||
let nz = vz + dz as f64 * step;
|
||||
|
||||
if octree.get_voxel_at_world_coords(nx, ny, nz).is_none() {
|
||||
let (normal, local_offset) = crate::systems::voxels::helper::face_orientation(dx, dy, dz, voxel_size_f);
|
||||
|
||||
// The voxel corner in chunk-local coords
|
||||
let voxel_corner_local = Vec3::new(vx as f32, vy as f32, vz as f32)
|
||||
- chunk_min_f32;
|
||||
|
||||
// generate face
|
||||
// e.g. center might be the corner + 0.5 offset, or
|
||||
// we can just treat the corner as the "center" in your face calc
|
||||
// but let's do it carefully:
|
||||
let face_center_local = voxel_corner_local + Vec3::splat(voxel_size_f*0.5);
|
||||
|
||||
let face_mesh = generate_face(
|
||||
normal,
|
||||
local_offset,
|
||||
face_center_local,
|
||||
voxel_size_f / 2.0,
|
||||
);
|
||||
face_meshes.push(face_mesh);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
face_meshes
|
||||
}
|
||||
|
||||
|
||||
|
||||
fn generate_face(orientation: Vec3, local_position: Vec3, position: Vec3, face_size: f32) -> Mesh {
|
||||
// Initialize an empty mesh with triangle topology
|
||||
let mut mesh = Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::default());
|
||||
|
||||
|
||||
let mut positions = vec![
|
||||
[-face_size, -face_size, 0.0],
|
||||
[face_size, -face_size, 0.0],
|
||||
[face_size, face_size, 0.0],
|
||||
[-face_size, face_size, 0.0],
|
||||
];
|
||||
|
||||
let rotation = Quat::from_rotation_arc(Vec3::Z, orientation);
|
||||
|
||||
// Rotate and translate the vertices based on orientation and position
|
||||
for p in positions.iter_mut() {
|
||||
let vertex = rotation * Vec3::from(*p);
|
||||
let vertex = vertex + local_position + position; // Apply local and global translation
|
||||
*p = [vertex.x, vertex.y, vertex.z];
|
||||
}
|
||||
|
||||
let uvs = vec![[0.0, 1.0], [1.0, 1.0], [1.0, 0.0], [0.0, 0.0]];
|
||||
|
||||
let indices = Indices::U32(vec![0, 1, 2, 2, 3, 0]);
|
||||
|
||||
let normal = rotation * Vec3::Z; // Since face is aligned to Vec3::Z initially
|
||||
let normals = vec![
|
||||
[normal.x, normal.y, normal.z]; // Use the same normal for all vertices
|
||||
4 // Four vertices in a quad
|
||||
];
|
||||
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals);
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs);
|
||||
mesh.insert_indices(indices);
|
||||
|
||||
mesh
|
||||
}
|
||||
fn merge_meshes(meshes: Vec<Mesh>) -> Mesh {
|
||||
let mut merged_positions = Vec::new();
|
||||
let mut merged_uvs = Vec::new();
|
||||
let mut merged_normals = Vec::new(); // To store merged normals
|
||||
let mut merged_indices = Vec::new();
|
||||
|
||||
for mesh in meshes {
|
||||
if let Some(VertexAttributeValues::Float32x3(positions)) = mesh.attribute(Mesh::ATTRIBUTE_POSITION) {
|
||||
let start_index = merged_positions.len();
|
||||
merged_positions.extend_from_slice(positions);
|
||||
|
||||
// Extract UVs
|
||||
if let Some(VertexAttributeValues::Float32x2(uvs)) = mesh.attribute(Mesh::ATTRIBUTE_UV_0) {
|
||||
merged_uvs.extend_from_slice(uvs);
|
||||
}
|
||||
|
||||
// Extract normals
|
||||
if let Some(VertexAttributeValues::Float32x3(normals)) = mesh.attribute(Mesh::ATTRIBUTE_NORMAL) {
|
||||
merged_normals.extend_from_slice(normals);
|
||||
}
|
||||
|
||||
// Extract indices and apply offset
|
||||
if let Some(indices) = mesh.indices() {
|
||||
if let Indices::U32(indices) = indices {
|
||||
let offset_indices: Vec<u32> = indices.iter().map(|i| i + start_index as u32).collect();
|
||||
merged_indices.extend(offset_indices);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create new merged mesh
|
||||
let mut merged_mesh = Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::default());
|
||||
|
||||
// Insert attributes into the merged mesh
|
||||
merged_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, merged_positions);
|
||||
merged_mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, merged_uvs);
|
||||
merged_mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, merged_normals); // Insert merged normals
|
||||
merged_mesh.insert_indices(Indices::U32(merged_indices));
|
||||
|
||||
merged_mesh
|
||||
}
|
||||
|
||||
107
src/systems/voxels/structure.rs
Normal file
107
src/systems/voxels/structure.rs
Normal file
@ -0,0 +1,107 @@
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use bevy::color::Color;
|
||||
use bevy::math::{DVec3, Vec2};
|
||||
use bevy::prelude::{Component, Entity, Resource, Vec3};
|
||||
use bevy_reflect::Reflect;
|
||||
|
||||
/// Represents a single voxel with a color.
|
||||
#[derive(Debug, Clone, Copy, Component, PartialEq, Default)]
|
||||
pub struct Voxel {
|
||||
pub color: Color,
|
||||
pub position: Vec3,
|
||||
}
|
||||
|
||||
/// Represents a node in the sparse voxel octree.
|
||||
|
||||
#[derive(Debug, Component, Clone)]
|
||||
pub struct OctreeNode {
|
||||
pub children: Option<Box<[OctreeNode; 8]>>,
|
||||
pub voxel: Option<Voxel>,
|
||||
pub is_leaf: bool,
|
||||
}
|
||||
/// Represents the root of the sparse voxel octree.
|
||||
/// Represents the root of the sparse voxel octree.
|
||||
#[derive(Debug, Component, Reflect)]
|
||||
#[reflect(from_reflect = false)]
|
||||
pub struct SparseVoxelOctree {
|
||||
|
||||
#[reflect(ignore)]
|
||||
pub root: OctreeNode,
|
||||
pub max_depth: u32,
|
||||
pub size: f64,
|
||||
pub show_wireframe: bool,
|
||||
pub show_world_grid: bool,
|
||||
pub show_chunks: bool,
|
||||
pub dirty_chunks: HashSet<(i32, i32, i32)>,
|
||||
}
|
||||
|
||||
#[derive(Default, Resource, Reflect)]
|
||||
pub struct ChunkEntities {
|
||||
pub map: HashMap<(i32, i32, i32), Entity>,
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct ChunkMarker {
|
||||
pub(crate) chunk_coords: (i64, i64, i64),
|
||||
}
|
||||
|
||||
|
||||
pub const CHUNK_RENDER_DISTANCE: f64 = 12.0;
|
||||
pub const CHUNK_BUILD_BUDGET: usize = 10;
|
||||
|
||||
|
||||
impl OctreeNode {
|
||||
/// Creates a new empty octree node.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
children: None,
|
||||
voxel: None,
|
||||
is_leaf: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.voxel.is_none() && self.children.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
impl Voxel {
|
||||
/// Creates a new empty octree node.
|
||||
pub fn new(color: Color) -> Self {
|
||||
Self {
|
||||
color,
|
||||
position: Vec3::ZERO
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub const NEIGHBOR_OFFSETS: [(f64, f64, f64); 6] = [
|
||||
(-1.0, 0.0, 0.0), // Left
|
||||
(1.0, 0.0, 0.0), // Right
|
||||
(0.0, -1.0, 0.0), // Down
|
||||
(0.0, 1.0, 0.0), // Up
|
||||
(0.0, 0.0, -1.0), // Back
|
||||
(0.0, 0.0, 1.0), // Front
|
||||
];
|
||||
|
||||
pub const CHUNK_NEIGHBOR_OFFSETS: [(i32, i32, i32); 6] = [
|
||||
(-1, 0, 0),
|
||||
(1, 0, 0),
|
||||
(0, -1, 0),
|
||||
(0, 1, 0),
|
||||
(0, 0, -1),
|
||||
(0, 0, 1),
|
||||
];
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Ray {
|
||||
pub origin: Vec3,
|
||||
pub direction: Vec3,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AABB {
|
||||
pub min: Vec3,
|
||||
pub max: Vec3,
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user