Merge pull request #16 from eliasstepanik/codex/verify-chunk-mesh-visibility-logic
Codex/verify chunk mesh visibility logic
2
.idea/runConfigurations/PublishServer_Win.xml
generated
@ -1,7 +1,7 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name="PublishServer Win" type="ShConfigurationType">
|
<configuration default="false" name="PublishServer Win" type="ShConfigurationType">
|
||||||
<option name="SCRIPT_TEXT" value="" />
|
<option name="SCRIPT_TEXT" value="" />
|
||||||
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
|
<option name="INDEPENDENT_SCRIPT_PATH" value="false" />
|
||||||
<option name="SCRIPT_PATH" value="$PROJECT_DIR$/publish_server.bat" />
|
<option name="SCRIPT_PATH" value="$PROJECT_DIR$/publish_server.bat" />
|
||||||
<option name="SCRIPT_OPTIONS" value="" />
|
<option name="SCRIPT_OPTIONS" value="" />
|
||||||
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
|
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
|
||||||
|
|||||||
4
.idea/runConfigurations/Run_horror_game.xml
generated
@ -1,7 +1,7 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name="Run horror-game" type="CargoCommandRunConfiguration" factoryName="Cargo Command" singleton="false">
|
<configuration default="false" name="Run horror-game" type="CargoCommandRunConfiguration" factoryName="Cargo Command" singleton="false">
|
||||||
<option name="buildProfileId" value="dev" />
|
<option name="buildProfileId" value="release" />
|
||||||
<option name="command" value="run --package horror-game --bin horror-game" />
|
<option name="command" value="run --package horror-game --bin horror-game --features bevy/trace_tracy_memory" />
|
||||||
<option name="workingDirectory" value="file://$PROJECT_DIR$/client" />
|
<option name="workingDirectory" value="file://$PROJECT_DIR$/client" />
|
||||||
<envs />
|
<envs />
|
||||||
<option name="emulateTerminal" value="true" />
|
<option name="emulateTerminal" value="true" />
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = ["client", "server"]
|
members = ["client"]
|
||||||
|
|||||||
@ -10,17 +10,13 @@ build = "build.rs"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bevy = { version = "0.15.1", features = ["jpeg", "trace_tracy", "trace_tracy_memory"] }
|
bevy = { version = "0.15.1", features = ["jpeg", "trace_tracy", "trace_tracy_memory"] }
|
||||||
bevy_egui = "0.33.0"
|
|
||||||
bevy_asset = "0.15.0"
|
|
||||||
bevy_reflect = "0.15.0"
|
|
||||||
bevy_render = "0.15.0"
|
|
||||||
bevy_window = "0.15.0"
|
|
||||||
egui_tiles = "0.12.0"
|
|
||||||
spacetimedb-sdk = "1.0"
|
|
||||||
hex = "0.4"
|
|
||||||
random_word = { version = "0.5.0", features = ["en"] }
|
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
|
big_space = "0.9.1"
|
||||||
|
noise = "0.9.0"
|
||||||
|
itertools = "0.13.0"
|
||||||
|
bitvec = "1.0.1"
|
||||||
|
smallvec = "1.14.0"
|
||||||
|
once_cell = "1.21.3"
|
||||||
|
rayon = "1.10.0"
|
||||||
3
client/assets/textures/generate_skybox.bat
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
#https://github.com/KhronosGroup/KTX-Software/releases
|
||||||
|
|
||||||
|
toktx --t2 --cubemap --target_type RGBA --zcmp 18 --genmipmap sky.ktx2 right.jpg left.jpg top.jpg bottom.jpg front.jpg back.jpg
|
||||||
BIN
client/assets/textures/skybox_landscape/back.jpg
Normal file
|
After Width: | Height: | Size: 723 KiB |
BIN
client/assets/textures/skybox_landscape/bottom.jpg
Normal file
|
After Width: | Height: | Size: 274 KiB |
BIN
client/assets/textures/skybox_landscape/front.jpg
Normal file
|
After Width: | Height: | Size: 462 KiB |
BIN
client/assets/textures/skybox_landscape/left.jpg
Normal file
|
After Width: | Height: | Size: 588 KiB |
BIN
client/assets/textures/skybox_landscape/right.jpg
Normal file
|
After Width: | Height: | Size: 525 KiB |
BIN
client/assets/textures/skybox_landscape/sky.ktx2
Normal file
BIN
client/assets/textures/skybox_landscape/top.jpg
Normal file
|
After Width: | Height: | Size: 338 KiB |
BIN
client/assets/textures/skybox_space/back.png
Normal file
|
After Width: | Height: | Size: 4.7 MiB |
BIN
client/assets/textures/skybox_space/bottom.png
Normal file
|
After Width: | Height: | Size: 5.2 MiB |
BIN
client/assets/textures/skybox_space/front.png
Normal file
|
After Width: | Height: | Size: 4.9 MiB |
BIN
client/assets/textures/skybox_space/left.png
Normal file
|
After Width: | Height: | Size: 4.7 MiB |
BIN
client/assets/textures/skybox_space/right.png
Normal file
|
After Width: | Height: | Size: 5.2 MiB |
BIN
client/assets/textures/skybox_space/sky.ktx2
Normal file
BIN
client/assets/textures/skybox_space/top.png
Normal file
|
After Width: | Height: | Size: 4.0 MiB |
BIN
client/assets/textures/skybox_space_1024/back.png
Normal file
|
After Width: | Height: | Size: 659 KiB |
BIN
client/assets/textures/skybox_space_1024/bottom.png
Normal file
|
After Width: | Height: | Size: 740 KiB |
BIN
client/assets/textures/skybox_space_1024/front.png
Normal file
|
After Width: | Height: | Size: 683 KiB |
BIN
client/assets/textures/skybox_space_1024/left.png
Normal file
|
After Width: | Height: | Size: 672 KiB |
BIN
client/assets/textures/skybox_space_1024/right.png
Normal file
|
After Width: | Height: | Size: 711 KiB |
BIN
client/assets/textures/skybox_space_1024/sky.ktx2
Normal file
BIN
client/assets/textures/skybox_space_1024/top.png
Normal file
|
After Width: | Height: | Size: 568 KiB |
@ -5,6 +5,6 @@ fn main() {
|
|||||||
let out_dir = std::env::var("OUT_DIR").unwrap();
|
let out_dir = std::env::var("OUT_DIR").unwrap();
|
||||||
let target_dir = Path::new(&out_dir).ancestors().nth(3).unwrap(); // gets target/debug or release
|
let target_dir = Path::new(&out_dir).ancestors().nth(3).unwrap(); // gets target/debug or release
|
||||||
|
|
||||||
fs::copy("config.toml", target_dir.join("config.toml"))
|
fs::copy("Config.toml", target_dir.join("Config.toml"))
|
||||||
.expect("Failed to copy config.toml to target directory");
|
.expect("Failed to copy Config.toml to target directory");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
use bevy::pbr::wireframe::WireframePlugin;
|
||||||
use crate::helper::debug_gizmos::debug_gizmos;
|
use crate::helper::debug_gizmos::debug_gizmos;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
pub struct AppPlugin;
|
pub struct AppPlugin;
|
||||||
@ -5,9 +6,11 @@ pub struct AppPlugin;
|
|||||||
impl Plugin for AppPlugin {
|
impl Plugin for AppPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.add_plugins(crate::plugins::ui::ui_plugin::UiPlugin);
|
app.add_plugins(crate::plugins::ui::ui_plugin::UiPlugin);
|
||||||
|
app.add_plugins(crate::plugins::big_space::big_space_plugin::BigSpaceIntegrationPlugin);
|
||||||
app.add_plugins(crate::plugins::environment::environment_plugin::EnvironmentPlugin);
|
app.add_plugins(crate::plugins::environment::environment_plugin::EnvironmentPlugin);
|
||||||
//app.add_plugins(crate::plugins::network::network_plugin::NetworkPlugin);
|
//app.add_plugins(crate::plugins::network::network_plugin::NetworkPlugin);
|
||||||
app.add_plugins(crate::plugins::input::input_plugin::InputPlugin);
|
app.add_plugins(crate::plugins::input::input_plugin::InputPlugin);
|
||||||
|
app.add_plugins(WireframePlugin);
|
||||||
|
|
||||||
app.add_systems(Update, (debug_gizmos));
|
app.add_systems(Update, (debug_gizmos));
|
||||||
app.register_type::<Option<Handle<Image>>>();
|
app.register_type::<Option<Handle<Image>>>();
|
||||||
|
|||||||
@ -1,3 +1,2 @@
|
|||||||
pub mod debug_gizmos;
|
pub mod debug_gizmos;
|
||||||
pub mod vector_helper;
|
|
||||||
pub mod math;
|
pub mod math;
|
||||||
@ -1,30 +0,0 @@
|
|||||||
use bevy::math::Vec3;
|
|
||||||
use bevy::prelude::{Quat, Transform};
|
|
||||||
use rand::Rng;
|
|
||||||
use crate::helper::math::RoundTo;
|
|
||||||
use crate::module_bindings::DbTransform;
|
|
||||||
|
|
||||||
pub(crate) fn random_vec3(min: f32, max: f32) -> Vec3 {
|
|
||||||
let mut rng = rand::thread_rng();
|
|
||||||
Vec3::new(
|
|
||||||
rng.gen_range(min..max),
|
|
||||||
rng.gen_range(min..max),
|
|
||||||
rng.gen_range(min..max),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DbTransform> for Transform {
|
|
||||||
fn from(db: DbTransform) -> Self {
|
|
||||||
Transform {
|
|
||||||
translation: Vec3::new(db.position.x, db.position.y, db.position.z),
|
|
||||||
rotation: //Quat::from_xyzw(0.0, 0.0, 0.0, 0.0),
|
|
||||||
Quat::from_xyzw(
|
|
||||||
db.rotation.x.round_to(3),
|
|
||||||
db.rotation.y.round_to(3),
|
|
||||||
db.rotation.z.round_to(3),
|
|
||||||
db.rotation.w.round_to(3),
|
|
||||||
),
|
|
||||||
scale: Vec3::new(db.scale.x.round_to(3), db.scale.y.round_to(3), db.scale.z.round_to(3)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,21 +1,22 @@
|
|||||||
mod app;
|
mod app;
|
||||||
mod helper;
|
mod helper;
|
||||||
mod plugins;
|
mod plugins;
|
||||||
mod module_bindings;
|
|
||||||
mod config;
|
mod config;
|
||||||
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use crate::app::AppPlugin;
|
use crate::app::AppPlugin;
|
||||||
use bevy::gizmos::{AppGizmoBuilder, GizmoPlugin};
|
use bevy::gizmos::{AppGizmoBuilder, GizmoPlugin};
|
||||||
use bevy::log::info;
|
use bevy::log::info;
|
||||||
use bevy::prelude::{default, App, GizmoConfigGroup, PluginGroup, Reflect, Res, Resource};
|
use bevy::prelude::*;
|
||||||
use bevy::render::settings::{Backends, RenderCreation, WgpuSettings};
|
use bevy::render::settings::{Backends, RenderCreation, WgpuSettings};
|
||||||
use bevy::render::RenderPlugin;
|
use bevy::render::RenderPlugin;
|
||||||
use bevy::DefaultPlugins;
|
use bevy::DefaultPlugins;
|
||||||
use bevy_egui::EguiPlugin;
|
use bevy::input::gamepad::AxisSettingsError::DeadZoneUpperBoundGreaterThanLiveZoneUpperBound;
|
||||||
use bevy_window::{PresentMode, Window, WindowPlugin};
|
use bevy::window::PresentMode;
|
||||||
|
use big_space::plugin::BigSpacePlugin;
|
||||||
use toml;
|
use toml;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
use crate::plugins::big_space::big_space_plugin::BigSpaceIntegrationPlugin;
|
||||||
|
|
||||||
const TITLE: &str = "horror-game";
|
const TITLE: &str = "horror-game";
|
||||||
const RESOLUTION: (f32, f32) = (1920f32, 1080f32);
|
const RESOLUTION: (f32, f32) = (1920f32, 1080f32);
|
||||||
@ -40,7 +41,7 @@ fn main() {
|
|||||||
register_platform_plugins(&mut app);
|
register_platform_plugins(&mut app);
|
||||||
|
|
||||||
app.add_plugins(AppPlugin);
|
app.add_plugins(AppPlugin);
|
||||||
app.add_plugins(EguiPlugin);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -76,7 +77,7 @@ fn register_platform_plugins(app: &mut App) {
|
|||||||
..default()
|
..default()
|
||||||
}),
|
}),
|
||||||
..default()
|
..default()
|
||||||
}),
|
}).build().disable::<TransformPlugin>(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
77
client/src/plugins/big_space/big_space_plugin.rs
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
use bevy::math::DVec3;
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use big_space::prelude::*;
|
||||||
|
|
||||||
|
/// Plugin enabling high precision coordinates using `big_space`.
|
||||||
|
///
|
||||||
|
/// This sets up [`BigSpacePlugin`] so entities can be placed far from the origin
|
||||||
|
/// without losing precision.
|
||||||
|
// ── plugin that creates the grid ──────────────────────────────────────────────
|
||||||
|
pub struct BigSpaceIntegrationPlugin;
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct RootGrid(pub Entity);
|
||||||
|
|
||||||
|
impl Plugin for BigSpaceIntegrationPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_plugins(BigSpacePlugin::<i64>::default());
|
||||||
|
|
||||||
|
app.add_systems(PreStartup, (spawn_root, cache_root.after(spawn_root)));
|
||||||
|
app.add_systems(PostStartup, (fix_invalid_children));
|
||||||
|
|
||||||
|
app.add_systems(PostUpdate,(fix_invalid_children));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) build the Big-Space root
|
||||||
|
fn spawn_root(mut commands: Commands) {
|
||||||
|
commands.spawn_big_space_default::<i64>(|_| {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) cache the root entity for later use
|
||||||
|
fn cache_root(
|
||||||
|
mut commands: Commands,
|
||||||
|
roots: Query<Entity, (With<BigSpace>, Without<Parent>)>, // top-level grid
|
||||||
|
) {
|
||||||
|
if let Ok(entity) = roots.get_single() {
|
||||||
|
|
||||||
|
commands.entity(entity).insert(Visibility::Visible);
|
||||||
|
commands.insert_resource(RootGrid(entity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fix_invalid_children(
|
||||||
|
mut commands: Commands,
|
||||||
|
bad: Query<Entity, (With<FloatingOrigin>, Without<GridCell<i64>>, With<Parent>)>,
|
||||||
|
) {
|
||||||
|
for e in &bad {
|
||||||
|
commands.entity(e).insert(GridCell::<i64>::ZERO);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn move_by(
|
||||||
|
mut q: Query<&mut Transform>,
|
||||||
|
delta: Vec3, // metres inside the current cell
|
||||||
|
) {
|
||||||
|
for mut t in &mut q {
|
||||||
|
t.translation += delta; // small numbers only
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
pub fn teleport_to<P: GridPrecision>(
|
||||||
|
e: Entity,
|
||||||
|
target: DVec3,
|
||||||
|
grids: Grids<'_, '_, P>,
|
||||||
|
mut q: Query<(&Parent, &mut GridCell<P>, &mut Transform)>,
|
||||||
|
) {
|
||||||
|
let (parent, mut cell, mut tf) = q.get_mut(e).unwrap();
|
||||||
|
let grid = grids.parent_grid(parent.get()).unwrap();
|
||||||
|
|
||||||
|
let (new_cell, local) = grid.translation_to_grid(target);
|
||||||
|
|
||||||
|
*cell = new_cell;
|
||||||
|
tf.translation = local;
|
||||||
|
}
|
||||||
2
client/src/plugins/big_space/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
pub mod big_space_plugin;
|
||||||
@ -1,13 +1,79 @@
|
|||||||
use bevy::app::{App, Plugin, PreStartup, PreUpdate, Startup};
|
use bevy::app::{App, Plugin, PreStartup, PreUpdate, Startup};
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use crate::plugins::environment::systems::voxels::culling::{despawn_distant_chunks};
|
||||||
|
use crate::plugins::environment::systems::voxels::debug::{draw_grid, visualize_octree_system};
|
||||||
|
use crate::plugins::environment::systems::voxels::queue_systems;
|
||||||
|
use crate::plugins::environment::systems::voxels::queue_systems::{enqueue_visible_chunks, process_chunk_queue};
|
||||||
|
use crate::plugins::environment::systems::voxels::render_chunks::rebuild_dirty_chunks;
|
||||||
|
use crate::plugins::environment::systems::voxels::lod::update_chunk_lods;
|
||||||
|
use crate::plugins::environment::systems::voxels::structure::{ChunkBudget, ChunkCullingCfg, ChunkQueue, SparseVoxelOctree, SpawnedChunks, PrevCameraChunk};
|
||||||
|
|
||||||
pub struct EnvironmentPlugin;
|
pub struct EnvironmentPlugin;
|
||||||
impl Plugin for EnvironmentPlugin {
|
impl Plugin for EnvironmentPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
|
|
||||||
app.add_systems(
|
app.add_systems(
|
||||||
Startup,
|
Startup,
|
||||||
(crate::plugins::environment::systems::environment_system::setup, crate::plugins::environment::systems::camera_system::setup ),
|
(
|
||||||
|
crate::plugins::environment::systems::camera_system::setup,
|
||||||
|
crate::plugins::environment::systems::environment_system::setup.after(crate::plugins::environment::systems::camera_system::setup),
|
||||||
|
crate::plugins::environment::systems::voxel_system::setup
|
||||||
|
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let view_distance_chunks = 100;
|
||||||
|
app.insert_resource(ChunkCullingCfg { view_distance_chunks });
|
||||||
|
app.insert_resource(ChunkBudget { per_frame: 20 });
|
||||||
|
app.init_resource::<PrevCameraChunk>();
|
||||||
|
app.add_systems(Update, log_mesh_count);
|
||||||
|
app
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// resources
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
.init_resource::<ChunkQueue>()
|
||||||
|
.init_resource::<SpawnedChunks>()
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// frame update
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
.add_systems(
|
||||||
|
Update,
|
||||||
|
(
|
||||||
|
/* ---------- culling & streaming ---------- */
|
||||||
|
despawn_distant_chunks, // 1. remove too-far chunks
|
||||||
|
enqueue_visible_chunks.after(despawn_distant_chunks), // 2. find new visible ones
|
||||||
|
process_chunk_queue .after(enqueue_visible_chunks), // 3. spawn ≤ budget per frame
|
||||||
|
update_chunk_lods.after(process_chunk_queue),
|
||||||
|
rebuild_dirty_chunks .after(process_chunk_queue), // 4. (re)mesh dirty chunks
|
||||||
|
|
||||||
|
/* ---------- optional debug drawing ------- */
|
||||||
|
visualize_octree_system
|
||||||
|
.run_if(should_visualize_octree)
|
||||||
|
.after(rebuild_dirty_chunks),
|
||||||
|
draw_grid
|
||||||
|
.run_if(should_draw_grid)
|
||||||
|
.after(visualize_octree_system),
|
||||||
|
)
|
||||||
|
.chain(), // make the whole tuple execute in this exact order
|
||||||
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn log_mesh_count(meshes: Res<Assets<Mesh>>, time: Res<Time>) {
|
||||||
|
if time.delta_secs_f64() as i32 % 5 == 0 {
|
||||||
|
info!("meshes: {}", meshes.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@ -1,13 +1,11 @@
|
|||||||
|
use bevy::core_pipeline::Skybox;
|
||||||
use bevy::input::mouse::{MouseMotion, MouseWheel};
|
use bevy::input::mouse::{MouseMotion, MouseWheel};
|
||||||
use bevy::math::Vec3;
|
use bevy::math::Vec3;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy_render::camera::{Exposure, PhysicalCameraParameters, Projection};
|
use bevy::render::camera::{Exposure, PhysicalCameraParameters};
|
||||||
use bevy_window::CursorGrabMode;
|
use big_space::prelude::{BigSpaceCommands, FloatingOrigin};
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use random_word::Lang;
|
use crate::plugins::big_space::big_space_plugin::RootGrid;
|
||||||
use crate::module_bindings::{set_name, set_position, spawn_entity, DbTransform, DbVector3, DbVector4};
|
|
||||||
use crate::plugins::network::systems::database::DbConnectionResource;
|
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub struct CameraController {
|
pub struct CameraController {
|
||||||
@ -27,12 +25,20 @@ impl Default for CameraController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn setup(mut commands: Commands,
|
||||||
pub fn setup(mut commands: Commands,) {
|
root: Res<RootGrid>,
|
||||||
|
asset_server: Res<AssetServer>) {
|
||||||
|
|
||||||
|
|
||||||
commands.spawn((
|
|
||||||
Transform::from_xyz(0.0, 0.0, 10.0), // initial f32
|
let cubemap_handle = asset_server.load("textures/skybox_space_1024/sky.ktx2");
|
||||||
|
commands.insert_resource(PendingSkybox { handle: cubemap_handle.clone() });
|
||||||
|
|
||||||
|
commands.entity(root.0).with_children(|parent| {
|
||||||
|
parent.spawn((
|
||||||
|
|
||||||
|
Name::new("Camera"),
|
||||||
|
Transform::from_xyz(0.0, 0.0, 10.0), // initial position
|
||||||
GlobalTransform::default(),
|
GlobalTransform::default(),
|
||||||
Camera3d::default(),
|
Camera3d::default(),
|
||||||
Projection::from(PerspectiveProjection {
|
Projection::from(PerspectiveProjection {
|
||||||
@ -46,7 +52,21 @@ pub fn setup(mut commands: Commands,) {
|
|||||||
sensitivity_iso: 100.0,
|
sensitivity_iso: 100.0,
|
||||||
sensor_height: 0.01866,
|
sensor_height: 0.01866,
|
||||||
}),
|
}),
|
||||||
|
FloatingOrigin,
|
||||||
|
Skybox {
|
||||||
|
image: cubemap_handle.clone(),
|
||||||
|
brightness: 1000.0,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
));
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
struct PendingSkybox {
|
||||||
|
handle: Handle<Image>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,29 +1,74 @@
|
|||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
use big_space::prelude::{BigSpace, BigSpaceCommands, GridCell, GridCommands};
|
||||||
|
use crate::plugins::big_space::big_space_plugin::RootGrid;
|
||||||
|
|
||||||
|
/// Earth and a FIFA-size football (diameters, metres)
|
||||||
|
const EARTH_DIAM: f32 = 12_742_000.0; // 12 742 km
|
||||||
|
const BALL_DIAM: f32 = 0.22; // 22 cm
|
||||||
|
|
||||||
pub(crate) fn setup(
|
pub(crate) fn setup(
|
||||||
mut commands : Commands,
|
mut commands: Commands,
|
||||||
mut meshes: ResMut<Assets<Mesh>>,
|
mut meshes: ResMut<Assets<Mesh>>,
|
||||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||||
|
root: Res<RootGrid>,
|
||||||
) {
|
) {
|
||||||
|
// one unit-diameter sphere mesh, reused for every instance
|
||||||
|
let sphere_mesh = meshes.add(Sphere::new(0.5).mesh().ico(32).unwrap());
|
||||||
|
let mat = materials.add(StandardMaterial {
|
||||||
|
base_color: Color::srgb(0.6, 0.7, 0.8),
|
||||||
|
perceptual_roughness: 0.7,
|
||||||
|
..default()
|
||||||
|
});
|
||||||
|
|
||||||
// 2) directional light
|
// light (unchanged)
|
||||||
commands.spawn(DirectionalLightBundle {
|
commands.entity(root.0).with_children(|p| {
|
||||||
|
p.spawn(DirectionalLightBundle {
|
||||||
transform: Transform::from_rotation(Quat::from_euler(
|
transform: Transform::from_rotation(Quat::from_euler(
|
||||||
EulerRot::XYZ,
|
EulerRot::XYZ,
|
||||||
-std::f32::consts::FRAC_PI_4,
|
-std::f32::consts::FRAC_PI_4,
|
||||||
std::f32::consts::FRAC_PI_4,
|
0.0,
|
||||||
0.0,
|
0.0,
|
||||||
)),
|
)),
|
||||||
directional_light: DirectionalLight {
|
directional_light: DirectionalLight {
|
||||||
shadows_enabled: true,
|
shadows_enabled: true,
|
||||||
..Default::default()
|
..default()
|
||||||
},
|
},
|
||||||
..Default::default()
|
..default()
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*// ---------- spawn spheres from football-size up to Earth-size ----------
|
||||||
|
const N: usize = 10; // how many spheres
|
||||||
|
let log_min = BALL_DIAM.log10();
|
||||||
|
let log_max = EARTH_DIAM.log10();
|
||||||
|
|
||||||
|
let mut offset = 0.0_f32; // keep objects apart
|
||||||
|
commands.entity(root.0).with_children(|parent| {
|
||||||
|
for i in 0..N {
|
||||||
|
// log-spaced diameters
|
||||||
|
let t = i as f32 / (N as f32 - 1.0);
|
||||||
|
let diam = 10f32.powf(log_min + t * (log_max - log_min));
|
||||||
|
let radius = diam * 0.5;
|
||||||
|
let scale_v3 = Vec3::splat(radius); // unit sphere → real size
|
||||||
|
|
||||||
|
// place the sphere so they don’t overlap
|
||||||
|
offset += radius; // move by previous radius
|
||||||
|
let pos = Vec3::new(offset, radius, 0.0); // sit on X axis, resting on Y=0
|
||||||
|
offset += radius + radius * 0.05; // add gap (5 %)
|
||||||
|
|
||||||
|
parent.spawn((
|
||||||
|
// spatial requirements for big_space
|
||||||
|
GridCell::<i64>::ZERO,
|
||||||
|
Transform::from_scale(scale_v3).with_translation(pos),
|
||||||
|
GlobalTransform::default(),
|
||||||
|
// rendering
|
||||||
|
Mesh3d(sphere_mesh.clone()),
|
||||||
|
MeshMaterial3d(mat.clone()),
|
||||||
|
Name::new(format!("Sphere_{i}")),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
});*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,2 +1,5 @@
|
|||||||
pub mod environment_system;
|
pub mod environment_system;
|
||||||
pub mod camera_system;
|
pub mod camera_system;
|
||||||
|
pub mod planet_system;
|
||||||
|
pub mod voxels;
|
||||||
|
pub mod voxel_system;
|
||||||
116
client/src/plugins/environment/systems/planet_system.rs
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
use bevy::asset::RenderAssetUsages;
|
||||||
|
use bevy::pbr::wireframe::{Wireframe, WireframeColor};
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy::render::mesh::*;
|
||||||
|
use big_space::floating_origins::FloatingOrigin;
|
||||||
|
use big_space::prelude::GridCell;
|
||||||
|
use noise::{Fbm, NoiseFn, Perlin};
|
||||||
|
use crate::plugins::big_space::big_space_plugin::RootGrid;
|
||||||
|
use crate::plugins::environment::systems::camera_system::CameraController;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct PlanetMaker;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct PlanetNoise(pub Fbm<Perlin>);
|
||||||
|
|
||||||
|
pub fn setup(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut meshes: ResMut<Assets<Mesh>>,
|
||||||
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||||
|
root: Res<RootGrid>,
|
||||||
|
) {
|
||||||
|
// Diameter ~ Earth (~12,742 km) × 2 to exaggerate terrain if desired
|
||||||
|
let radius = 12_742_000.0;
|
||||||
|
let sphere_mesh = meshes.add(
|
||||||
|
SphereMeshBuilder::new(radius, SphereKind::Ico { subdivisions: 100 })
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
let material_handle = materials.add(StandardMaterial::from(Color::rgb(0.3, 0.6, 1.0)));
|
||||||
|
|
||||||
|
commands.entity(root.0).with_children(|parent| {
|
||||||
|
parent.spawn((
|
||||||
|
Name::new("Planet"),
|
||||||
|
Mesh3d(sphere_mesh.clone()),
|
||||||
|
MeshMaterial3d(material_handle),
|
||||||
|
GridCell::<i64>::ZERO,
|
||||||
|
Transform::default(),
|
||||||
|
PlanetMaker,
|
||||||
|
Wireframe,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub(crate) fn setup_noise(mut commands: Commands) {
|
||||||
|
let fbm_noise = Fbm::<Perlin>::new(0);
|
||||||
|
commands.insert_resource(PlanetNoise(fbm_noise));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deform_planet(
|
||||||
|
mut meshes: ResMut<Assets<Mesh>>,
|
||||||
|
noise: Res<PlanetNoise>,
|
||||||
|
query: Query<&Mesh3d, With<PlanetMaker>>,
|
||||||
|
) {
|
||||||
|
let frequency = 4.0 / 12_742_000.0;
|
||||||
|
let amplitude = 100_000.0;
|
||||||
|
|
||||||
|
for mesh3d in query.iter() {
|
||||||
|
let handle: &Handle<Mesh> = &mesh3d.0;
|
||||||
|
|
||||||
|
if let Some(mesh) = meshes.get_mut(handle) {
|
||||||
|
// 1. Immutable borrow to extract normals (or default)
|
||||||
|
let normals: Vec<[f32; 3]> = if let Some(VertexAttributeValues::Float32x3(vals)) =
|
||||||
|
mesh.attribute(Mesh::ATTRIBUTE_NORMAL)
|
||||||
|
{
|
||||||
|
vals.clone()
|
||||||
|
} else {
|
||||||
|
// default normals if none exist
|
||||||
|
let count = mesh
|
||||||
|
.attribute(Mesh::ATTRIBUTE_POSITION)
|
||||||
|
.and_then(|attr| match attr {
|
||||||
|
VertexAttributeValues::Float32x3(v) => Some(v.len()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.unwrap_or(0);
|
||||||
|
vec![[0.0, 1.0, 0.0]; count]
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. Drop the immutable borrow, then mutable-borrow positions
|
||||||
|
if let Some(VertexAttributeValues::Float32x3(positions)) =
|
||||||
|
mesh.attribute_mut(Mesh::ATTRIBUTE_POSITION)
|
||||||
|
{
|
||||||
|
// Now mutate positions using the pre-fetched normals
|
||||||
|
for (i, pos) in positions.iter_mut().enumerate() {
|
||||||
|
let mut vertex = Vec3::new(pos[0], pos[1], pos[2]);
|
||||||
|
let normal = Vec3::new(
|
||||||
|
normals[i][0],
|
||||||
|
normals[i][1],
|
||||||
|
normals[i][2],
|
||||||
|
);
|
||||||
|
|
||||||
|
let unit_dir = vertex.normalize();
|
||||||
|
let sample = [
|
||||||
|
unit_dir.x as f64 * frequency as f64,
|
||||||
|
unit_dir.y as f64 * frequency as f64,
|
||||||
|
unit_dir.z as f64 * frequency as f64,
|
||||||
|
];
|
||||||
|
let noise_value = noise.0.get(sample) as f32;
|
||||||
|
let offset = normal * (noise_value * amplitude);
|
||||||
|
|
||||||
|
let new_pos = unit_dir * (vertex.length() + offset.length());
|
||||||
|
*pos = [new_pos.x, new_pos.y, new_pos.z];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
mesh.compute_smooth_normals();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force AABB recalc
|
||||||
|
mesh.remove_attribute(Mesh::ATTRIBUTE_COLOR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
185
client/src/plugins/environment/systems/voxel_system.rs
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
use crate::plugins::big_space::big_space_plugin::RootGrid;
|
||||||
|
use crate::plugins::environment::systems::voxels::structure::*;
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy::render::mesh::*;
|
||||||
|
use noise::{NoiseFn, Perlin};
|
||||||
|
|
||||||
|
pub fn setup(
|
||||||
|
mut commands: Commands,
|
||||||
|
root: Res<RootGrid>,
|
||||||
|
) {
|
||||||
|
let unit_size = 1.0_f32;
|
||||||
|
let octree_base_size = 64.0 * unit_size;
|
||||||
|
let octree_depth = 10;
|
||||||
|
|
||||||
|
// 1. Create octree and wrap in Arc<Mutex<>> for thread-safe generation
|
||||||
|
let mut octree = SparseVoxelOctree::new(octree_depth, octree_base_size, false, false, false);
|
||||||
|
|
||||||
|
// 2. Generate sphere in parallel, dropping the cloned Arc inside the function
|
||||||
|
let color = Color::rgb(0.2, 0.8, 0.2);
|
||||||
|
|
||||||
|
generate_voxel_sphere(&mut octree, 110, color);
|
||||||
|
|
||||||
|
// 4. Spawn entity with both Transform and the real octree component
|
||||||
|
commands.entity(root.0).with_children(|parent| {
|
||||||
|
parent.spawn((Transform::default(), octree));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn generate_voxel_sphere(
|
||||||
|
octree: &mut SparseVoxelOctree,
|
||||||
|
planet_radius: i32,
|
||||||
|
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;
|
||||||
|
let max = planet_radius;
|
||||||
|
|
||||||
|
let step = octree.get_spacing_at_depth(octree.max_depth);
|
||||||
|
|
||||||
|
for ix in min..=max {
|
||||||
|
let x = ix;
|
||||||
|
for iy in min..=max {
|
||||||
|
let y = iy;
|
||||||
|
for iz in min..=max {
|
||||||
|
let z = iz;
|
||||||
|
|
||||||
|
// 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 as f32 * step;
|
||||||
|
let wy = y as f32 * step;
|
||||||
|
let wz = z as f32 * step;
|
||||||
|
let position = Vec3::new(wx, wy, wz);
|
||||||
|
|
||||||
|
// Insert the voxel
|
||||||
|
let voxel = Voxel {
|
||||||
|
color: voxel_color,
|
||||||
|
};
|
||||||
|
octree.insert(position, 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 f32;
|
||||||
|
for iy in 0..size_y {
|
||||||
|
let y = iy as f32;
|
||||||
|
for iz in 0..size_z {
|
||||||
|
let z = iz as f32;
|
||||||
|
|
||||||
|
// Convert (x,y,z) to world coordinates
|
||||||
|
let wx = x * step;
|
||||||
|
let wy = y * step;
|
||||||
|
let wz = z * step;
|
||||||
|
|
||||||
|
let position = Vec3::new(wx, wy, wz);
|
||||||
|
|
||||||
|
// Insert the voxel
|
||||||
|
let voxel = Voxel {
|
||||||
|
color: voxel_color,
|
||||||
|
};
|
||||||
|
octree.insert(position, 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 f32;
|
||||||
|
for iz in 0..depth {
|
||||||
|
let z = iz as f32;
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
let position = Vec3::new(wx, wy, wz);
|
||||||
|
|
||||||
|
// Insert the voxel
|
||||||
|
let voxel = Voxel {
|
||||||
|
color,
|
||||||
|
};
|
||||||
|
octree.insert(position, voxel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn generate_solid_plane_with_noise(
|
||||||
|
octree: &mut SparseVoxelOctree,
|
||||||
|
width: usize,
|
||||||
|
depth: usize,
|
||||||
|
color: Color,
|
||||||
|
noise: &Perlin,
|
||||||
|
frequency: f32,
|
||||||
|
amplitude: f32,
|
||||||
|
) {
|
||||||
|
// Size of one voxel at the deepest level
|
||||||
|
let step = octree.get_spacing_at_depth(octree.max_depth);
|
||||||
|
|
||||||
|
for ix in 0..width {
|
||||||
|
let x = ix as f32;
|
||||||
|
for iz in 0..depth {
|
||||||
|
let z = iz as f32;
|
||||||
|
|
||||||
|
// Sample Perlin noise at scaled coordinates
|
||||||
|
let sample_x = x * frequency;
|
||||||
|
let sample_z = z * frequency;
|
||||||
|
let noise_val = noise.get([sample_x as f64, sample_z as f64]) as f32;
|
||||||
|
|
||||||
|
// Height in world units
|
||||||
|
let height_world = noise_val * amplitude;
|
||||||
|
// Convert height to number of voxel layers
|
||||||
|
let max_layer = (height_world / step).ceil() as usize;
|
||||||
|
|
||||||
|
// Fill from layer 0 up to max_layer
|
||||||
|
for iy in 0..=max_layer {
|
||||||
|
let position = Vec3::new(
|
||||||
|
x * step,
|
||||||
|
iy as f32 * step,
|
||||||
|
z * step,
|
||||||
|
);
|
||||||
|
|
||||||
|
let voxel = Voxel { color };
|
||||||
|
octree.insert(position, voxel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
32
client/src/plugins/environment/systems/voxels/chunk.rs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
use bevy::prelude::*;
|
||||||
|
use crate::plugins::environment::systems::voxels::structure::{ChunkKey, SparseVoxelOctree, Voxel, CHUNK_POW, CHUNK_SIZE};
|
||||||
|
|
||||||
|
/// Component attached to the entity that owns the mesh of one chunk.
|
||||||
|
|
||||||
|
impl SparseVoxelOctree {
|
||||||
|
pub fn chunk_has_any_voxel(&self, key: ChunkKey) -> bool {
|
||||||
|
// world-space centre of the chunk
|
||||||
|
let step = self.get_spacing_at_depth(self.max_depth);
|
||||||
|
let half = self.size * 0.5;
|
||||||
|
let centre = Vec3::new(
|
||||||
|
(key.0 as f32 + 0.5) * CHUNK_SIZE as f32 * step - half,
|
||||||
|
(key.1 as f32 + 0.5) * CHUNK_SIZE as f32 * step - half,
|
||||||
|
(key.2 as f32 + 0.5) * CHUNK_SIZE as f32 * step - half,
|
||||||
|
);
|
||||||
|
|
||||||
|
// depth of the octree node that exactly matches one chunk
|
||||||
|
let depth = self.max_depth.saturating_sub(CHUNK_POW);
|
||||||
|
|
||||||
|
// normalised coordinates of that centre at the chosen depth
|
||||||
|
let norm = self.normalize_to_voxel_at_depth(centre, depth);
|
||||||
|
|
||||||
|
// walk the tree down to that node …
|
||||||
|
if let Some(node) =
|
||||||
|
Self::get_node_at_depth(&self.root, norm.x, norm.y, norm.z, depth)
|
||||||
|
{
|
||||||
|
// … and ask whether that node or any child contains voxels
|
||||||
|
return self.has_volume(node);
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
41
client/src/plugins/environment/systems/voxels/culling.rs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
use std::collections::{HashMap, VecDeque};
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use crate::plugins::environment::systems::voxels::helper::world_to_chunk;
|
||||||
|
use crate::plugins::environment::systems::voxels::structure::*;
|
||||||
|
|
||||||
|
|
||||||
|
/// despawn (or hide) every chunk entity whose centre is farther away than the
|
||||||
|
/// configured radius
|
||||||
|
|
||||||
|
pub fn despawn_distant_chunks(
|
||||||
|
mut commands : Commands,
|
||||||
|
cam_q : Query<&GlobalTransform, With<Camera>>,
|
||||||
|
tree_q : Query<&SparseVoxelOctree>,
|
||||||
|
mut spawned : ResMut<SpawnedChunks>,
|
||||||
|
chunk_q : Query<(Entity,
|
||||||
|
&Chunk,
|
||||||
|
&Mesh3d,
|
||||||
|
&MeshMaterial3d<StandardMaterial>)>,
|
||||||
|
mut meshes : ResMut<Assets<Mesh>>,
|
||||||
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||||
|
cfg : Res<ChunkCullingCfg>,
|
||||||
|
) {
|
||||||
|
let tree = tree_q.single();
|
||||||
|
let cam = cam_q.single().translation();
|
||||||
|
let centre = world_to_chunk(tree, cam);
|
||||||
|
|
||||||
|
for (ent, chunk, mesh3d, mat3d) in chunk_q.iter() {
|
||||||
|
let ChunkKey(x, y, z) = chunk.key;
|
||||||
|
if (x - centre.0).abs() > cfg.view_distance_chunks ||
|
||||||
|
(y - centre.1).abs() > cfg.view_distance_chunks ||
|
||||||
|
(z - centre.2).abs() > cfg.view_distance_chunks {
|
||||||
|
|
||||||
|
// free assets – borrow, don't move
|
||||||
|
meshes.remove(&mesh3d.0);
|
||||||
|
materials.remove(&mat3d.0);
|
||||||
|
|
||||||
|
commands.entity(ent).despawn_recursive();
|
||||||
|
spawned.0.remove(&chunk.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
145
client/src/plugins/environment/systems/voxels/debug.rs
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
use bevy::prelude::*;
|
||||||
|
use crate::plugins::environment::systems::voxels::structure::*;
|
||||||
|
|
||||||
|
/// Visualize each node of the octree as a scaled cuboid, **center-based**.
|
||||||
|
/// `octree_tf.translation` is the world-space center of the root bounding box.
|
||||||
|
pub fn visualize_octree_system(
|
||||||
|
mut gizmos: Gizmos,
|
||||||
|
octree_query: Query<(&SparseVoxelOctree, &Transform)>,
|
||||||
|
) {
|
||||||
|
for (octree, octree_tf) in octree_query.iter() {
|
||||||
|
// The root node covers [-size/2..+size/2], so half_size is:
|
||||||
|
let half_size = octree.size * 0.5;
|
||||||
|
|
||||||
|
// Draw a translucent cuboid for the root
|
||||||
|
gizmos.cuboid(
|
||||||
|
Transform::from_translation(octree_tf.translation)
|
||||||
|
.with_scale(Vec3::splat(octree.size)),
|
||||||
|
Color::rgba(1.0, 1.0, 0.0, 0.15),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Recursively draw children:
|
||||||
|
// Start from depth=0. The node at depth=0 has bounding side = octree.size.
|
||||||
|
visualize_recursive_center(
|
||||||
|
&mut gizmos,
|
||||||
|
&octree.root,
|
||||||
|
octree_tf.translation, // center of root in world
|
||||||
|
octree.size,
|
||||||
|
0,
|
||||||
|
octree.max_depth,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recursively draws cuboids for each node.
|
||||||
|
/// We follow the same indexing as insert_recursive, i.e. bit patterns:
|
||||||
|
/// i=0 => child in (-x,-y,-z) quadrant,
|
||||||
|
/// i=1 => (+x,-y,-z), i=2 => (-x,+y,-z), etc.
|
||||||
|
fn visualize_recursive_center(
|
||||||
|
gizmos: &mut Gizmos,
|
||||||
|
node: &OctreeNode,
|
||||||
|
parent_center: Vec3,
|
||||||
|
parent_size: f32,
|
||||||
|
depth: u32,
|
||||||
|
max_depth: u32,
|
||||||
|
) {
|
||||||
|
if depth >= max_depth {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(children) = &node.children {
|
||||||
|
// Each child is half the parent’s size
|
||||||
|
let child_size = parent_size * 0.5;
|
||||||
|
let half = child_size * 0.5;
|
||||||
|
|
||||||
|
for (i, child) in children.iter().enumerate() {
|
||||||
|
// For i in [0..8], bits: x=1, y=2, z=4
|
||||||
|
let offset_x = if (i & 1) != 0 { half } else { -half };
|
||||||
|
let offset_y = if (i & 2) != 0 { half } else { -half };
|
||||||
|
let offset_z = if (i & 4) != 0 { half } else { -half };
|
||||||
|
|
||||||
|
let child_center = parent_center + Vec3::new(offset_x, offset_y, offset_z);
|
||||||
|
|
||||||
|
// Draw the child bounding box
|
||||||
|
gizmos.cuboid(
|
||||||
|
Transform::from_translation(child_center).with_scale(Vec3::splat(child_size)),
|
||||||
|
Color::rgba(0.5, 1.0, 0.5, 0.15), // greenish
|
||||||
|
);
|
||||||
|
|
||||||
|
// Recurse
|
||||||
|
visualize_recursive_center(
|
||||||
|
gizmos,
|
||||||
|
child,
|
||||||
|
child_center,
|
||||||
|
child_size,
|
||||||
|
depth + 1,
|
||||||
|
max_depth,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If node.is_leaf && node.voxel.is_some(), draw a smaller marker
|
||||||
|
if node.is_leaf {
|
||||||
|
if let Some(voxel) = node.voxel {
|
||||||
|
// We'll choose a size that's a fraction of the parent's size.
|
||||||
|
// For example, 25% of the parent bounding box dimension.
|
||||||
|
let leaf_size = parent_size * 0.25;
|
||||||
|
|
||||||
|
// Draw a small cuboid at the same center as the parent node.
|
||||||
|
gizmos.cuboid(
|
||||||
|
Transform::from_translation(parent_center)
|
||||||
|
.with_scale(Vec3::splat(leaf_size)),
|
||||||
|
voxel.color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn draw_grid(
|
||||||
|
mut gizmos: Gizmos,
|
||||||
|
camera_query: Query<&Transform, With<Camera>>,
|
||||||
|
octree_query: Query<(&SparseVoxelOctree, &Transform)>,
|
||||||
|
) {
|
||||||
|
let camera_tf = camera_query.single();
|
||||||
|
let camera_pos = camera_tf.translation;
|
||||||
|
|
||||||
|
for (octree, octree_tf) in octree_query.iter() {
|
||||||
|
let half_size = octree.size * 0.5;
|
||||||
|
let root_center = octree_tf.translation;
|
||||||
|
|
||||||
|
// Voxel spacing at max depth
|
||||||
|
let spacing = octree.get_spacing_at_depth(octree.max_depth);
|
||||||
|
let grid_count = (octree.size / spacing) as i32;
|
||||||
|
|
||||||
|
// We'll define the bounding region as [center-half_size .. center+half_size].
|
||||||
|
// So the min corner is (root_center - half_size).
|
||||||
|
let min_corner = root_center - Vec3::splat(half_size);
|
||||||
|
|
||||||
|
// Draw lines in X & Z directions (like a ground plane).
|
||||||
|
for i in 0..=grid_count {
|
||||||
|
let offset = i as f32 * spacing;
|
||||||
|
|
||||||
|
// 1) line along Z
|
||||||
|
let x = min_corner.x + offset;
|
||||||
|
let z1 = min_corner.z;
|
||||||
|
let z2 = min_corner.z + (grid_count as f32 * spacing);
|
||||||
|
|
||||||
|
let p1 = Vec3::new(x, min_corner.y, z1);
|
||||||
|
let p2 = Vec3::new(x, min_corner.y, z2);
|
||||||
|
|
||||||
|
// offset by -camera_pos for stable Gizmos in large coords
|
||||||
|
let p1_f32 = p1 - camera_pos;
|
||||||
|
let p2_f32 = p2 - camera_pos;
|
||||||
|
gizmos.line(p1_f32, p2_f32, Color::WHITE);
|
||||||
|
|
||||||
|
// 2) line along X
|
||||||
|
let z = min_corner.z + offset;
|
||||||
|
let x1 = min_corner.x;
|
||||||
|
let x2 = min_corner.x + (grid_count as f32 * spacing);
|
||||||
|
|
||||||
|
let p3 = Vec3::new(x1, min_corner.y, z) - camera_pos;
|
||||||
|
let p4 = Vec3::new(x2, min_corner.y, z) - camera_pos;
|
||||||
|
gizmos.line(p3, p4, Color::WHITE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
340
client/src/plugins/environment/systems/voxels/helper.rs
Normal file
@ -0,0 +1,340 @@
|
|||||||
|
use bevy::prelude::*;
|
||||||
|
use crate::plugins::environment::systems::voxels::structure::*;
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Returns the size of one voxel at the given depth.
|
||||||
|
pub fn get_spacing_at_depth(&self, depth: u32) -> f32 {
|
||||||
|
let effective = depth.min(self.max_depth);
|
||||||
|
self.size / (2_u32.pow(effective)) as f32
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Center-based: [-size/2..+size/2]. Shift +half_size => [0..size], floor, shift back.
|
||||||
|
pub fn normalize_to_voxel_at_depth(&self, position: Vec3, depth: u32) -> Vec3 {
|
||||||
|
// Convert world coordinate to normalized [0,1] space.
|
||||||
|
let half_size = self.size * 0.5;
|
||||||
|
// Shift to [0, self.size]
|
||||||
|
let shifted = (position + Vec3::splat(half_size)) / self.size;
|
||||||
|
// Determine the number of voxels along an edge at the given depth.
|
||||||
|
let voxel_count = 2_u32.pow(depth) as f32;
|
||||||
|
// Get the voxel index (as a float) and then compute the center in normalized space.
|
||||||
|
let voxel_index = (shifted * voxel_count).floor();
|
||||||
|
let voxel_center = (voxel_index + Vec3::splat(0.5)) / voxel_count;
|
||||||
|
voxel_center
|
||||||
|
}
|
||||||
|
pub fn denormalize_voxel_center(&self, voxel_center: Vec3) -> Vec3 {
|
||||||
|
let half_size = self.size * 0.5;
|
||||||
|
// Convert the normalized voxel center back to world space.
|
||||||
|
voxel_center * self.size - Vec3::splat(half_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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)> {
|
||||||
|
// Define a safe inverse function to avoid division by zero.
|
||||||
|
let safe_inv = |d: f32| if d.abs() < 1e-6 { 1e6 } else { 1.0 / d };
|
||||||
|
let inv_dir = Vec3::new(
|
||||||
|
safe_inv(ray.direction.x),
|
||||||
|
safe_inv(ray.direction.y),
|
||||||
|
safe_inv(ray.direction.z),
|
||||||
|
);
|
||||||
|
|
||||||
|
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 {
|
||||||
|
let epsilon = 1e-6;
|
||||||
|
let mut normal = Vec3::ZERO;
|
||||||
|
// Determine which face was hit by comparing t_enter to the computed values.
|
||||||
|
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 (x,y,z) is within [-size/2..+size/2].
|
||||||
|
pub fn contains(&self, x: f32, y: f32, z: f32) -> bool {
|
||||||
|
let half_size = self.size / 2.0;
|
||||||
|
let eps = 1e-6;
|
||||||
|
(x >= -half_size - eps && x < half_size + eps)
|
||||||
|
&& (y >= -half_size - eps && y < half_size + eps)
|
||||||
|
&& (z >= -half_size - eps && z < half_size + eps)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve a voxel at world coordinates by normalizing and looking up.
|
||||||
|
pub fn get_voxel_at_world_coords(&self, position: Vec3) -> Option<&Voxel> {
|
||||||
|
let aligned = self.normalize_to_voxel_at_depth(position, self.max_depth);
|
||||||
|
self.get_voxel_at(aligned.x, aligned.y, aligned.z)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn local_to_world(&self, local_pos: Vec3) -> Vec3 {
|
||||||
|
// Half the total octree size, used to shift the center to the origin.
|
||||||
|
let half_size = self.size * 0.5;
|
||||||
|
// Convert local coordinate to world space:
|
||||||
|
// 1. Subtract 0.5 to center the coordinate at zero (range becomes [-0.5, 0.5])
|
||||||
|
// 2. Multiply by the total size to scale into world units.
|
||||||
|
// 3. Add half_size to shift from a center–based system to one starting at zero.
|
||||||
|
(local_pos - Vec3::splat(0.5)) * self.size + Vec3::splat(half_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Helper function to recursively traverse the octree to a specific depth.
|
||||||
|
pub(crate) fn get_node_at_depth(
|
||||||
|
node: &OctreeNode,
|
||||||
|
x: f32,
|
||||||
|
y: f32,
|
||||||
|
z: f32,
|
||||||
|
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: f32| {
|
||||||
|
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 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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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: f32, dy: f32, dz: f32, 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn chunk_key_from_world(tree: &SparseVoxelOctree, pos: Vec3) -> ChunkKey {
|
||||||
|
let half = tree.size * 0.5;
|
||||||
|
|
||||||
|
let step = tree.get_spacing_at_depth(tree.max_depth);
|
||||||
|
let scale = CHUNK_SIZE as f32 * step; // metres per chunk
|
||||||
|
ChunkKey(
|
||||||
|
((pos.x + half) / scale).floor() as i32,
|
||||||
|
((pos.y + half) / scale).floor() as i32,
|
||||||
|
((pos.z + half) / scale).floor() as i32,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn world_to_chunk(tree: &SparseVoxelOctree, p: Vec3) -> ChunkKey {
|
||||||
|
let step = tree.get_spacing_at_depth(tree.max_depth);
|
||||||
|
let half = tree.size * 0.5;
|
||||||
|
let scale = CHUNK_SIZE as f32 * step;
|
||||||
|
ChunkKey(
|
||||||
|
((p.x + half) / scale).floor() as i32,
|
||||||
|
((p.y + half) / scale).floor() as i32,
|
||||||
|
((p.z + half) / scale).floor() as i32,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn chunk_center_world(tree: &SparseVoxelOctree, key: ChunkKey) -> Vec3 {
|
||||||
|
let half = tree.size * 0.5;
|
||||||
|
let step = tree.get_spacing_at_depth(tree.max_depth);
|
||||||
|
Vec3::new(
|
||||||
|
(key.0 as f32 + 0.5) * CHUNK_SIZE as f32 * step - half,
|
||||||
|
(key.1 as f32 + 0.5) * CHUNK_SIZE as f32 * step - half,
|
||||||
|
(key.2 as f32 + 0.5) * CHUNK_SIZE as f32 * step - half,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AABB {
|
||||||
|
pub fn intersects_aabb(&self, other: &AABB) -> bool {
|
||||||
|
self.min.x <= other.max.x &&
|
||||||
|
self.max.x >= other.min.x &&
|
||||||
|
self.min.y <= other.max.y &&
|
||||||
|
self.max.y >= other.min.y &&
|
||||||
|
self.min.z <= other.max.z &&
|
||||||
|
self.max.z >= other.min.z
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn center(&self) -> Vec3 {
|
||||||
|
(self.min + self.max) * 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SparseVoxelOctree {
|
||||||
|
pub fn collect_voxels_in_region(&self, min: Vec3, max: Vec3) -> Vec<(Vec3, Voxel)> {
|
||||||
|
let half_size = self.size * 0.5;
|
||||||
|
let root_bounds = AABB {
|
||||||
|
min: Vec3::new(-half_size, -half_size, -half_size),
|
||||||
|
max: Vec3::new(half_size, half_size, half_size),
|
||||||
|
};
|
||||||
|
let mut voxels = Vec::new();
|
||||||
|
self.collect_voxels_in_region_recursive(&self.root, root_bounds, min, max, &mut voxels);
|
||||||
|
voxels
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_voxels_in_region_recursive(
|
||||||
|
&self,
|
||||||
|
node: &OctreeNode,
|
||||||
|
node_bounds: AABB,
|
||||||
|
min: Vec3,
|
||||||
|
max: Vec3,
|
||||||
|
out: &mut Vec<(Vec3, Voxel)>,
|
||||||
|
) {
|
||||||
|
if !node_bounds.intersects_aabb(&AABB { min, max }) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.is_leaf {
|
||||||
|
if let Some(voxel) = &node.voxel {
|
||||||
|
let center = node_bounds.center();
|
||||||
|
if center.x >= min.x && center.x <= max.x &&
|
||||||
|
center.y >= min.y && center.y <= max.y &&
|
||||||
|
center.z >= min.z && center.z <= max.z
|
||||||
|
{
|
||||||
|
out.push((center, *voxel));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(children) = &node.children {
|
||||||
|
for (i, child) in children.iter().enumerate() {
|
||||||
|
let child_bounds = self.compute_child_bounds(&node_bounds, i);
|
||||||
|
self.collect_voxels_in_region_recursive(child, child_bounds, min, max, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
client/src/plugins/environment/systems/voxels/lod.rs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
use bevy::prelude::*;
|
||||||
|
use crate::plugins::environment::systems::voxels::helper::chunk_center_world;
|
||||||
|
use crate::plugins::environment::systems::voxels::structure::{Chunk, ChunkLod, ChunkCullingCfg, SparseVoxelOctree, CHUNK_SIZE};
|
||||||
|
|
||||||
|
/// Update each chunk's LOD level based on its distance from the camera.
|
||||||
|
/// Chunks farther away get a higher LOD value (coarser mesh).
|
||||||
|
pub fn update_chunk_lods(
|
||||||
|
cam_q: Query<&GlobalTransform, With<Camera>>,
|
||||||
|
mut chunks: Query<(&Chunk, &mut ChunkLod)>,
|
||||||
|
mut tree_q: Query<&mut SparseVoxelOctree>,
|
||||||
|
cfg: Res<ChunkCullingCfg>,
|
||||||
|
) {
|
||||||
|
let cam_pos = cam_q.single().translation();
|
||||||
|
|
||||||
|
// Borrow the octree only once to avoid repeated query lookups
|
||||||
|
let mut tree = tree_q.single_mut();
|
||||||
|
let max_depth = tree.max_depth;
|
||||||
|
let range_step = cfg.view_distance_chunks as f32 / max_depth as f32;
|
||||||
|
let chunk_size = CHUNK_SIZE as f32 * tree.get_spacing_at_depth(max_depth);
|
||||||
|
|
||||||
|
let mut changed = Vec::new();
|
||||||
|
for (chunk, mut lod) in chunks.iter_mut() {
|
||||||
|
let center = chunk_center_world(&tree, chunk.key);
|
||||||
|
let dist_chunks = cam_pos.distance(center) / chunk_size;
|
||||||
|
let mut level = (dist_chunks / range_step).floor() as u32;
|
||||||
|
if level > max_depth {
|
||||||
|
level = max_depth;
|
||||||
|
}
|
||||||
|
if lod.0 != level {
|
||||||
|
lod.0 = level;
|
||||||
|
changed.push(chunk.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for key in changed {
|
||||||
|
tree.dirty_chunks.insert(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
484
client/src/plugins/environment/systems/voxels/meshing.rs
Normal file
@ -0,0 +1,484 @@
|
|||||||
|
use bevy::asset::RenderAssetUsages;
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy::render::mesh::{Indices, PrimitiveTopology, VertexAttributeValues, Mesh};
|
||||||
|
use crate::plugins::environment::systems::voxels::structure::*;
|
||||||
|
|
||||||
|
/*pub(crate) fn mesh_chunk(
|
||||||
|
buffer: &[[[Option<Voxel>; CHUNK_SIZE as usize]; CHUNK_SIZE as usize]; CHUNK_SIZE as usize],
|
||||||
|
origin: Vec3,
|
||||||
|
step: f32,
|
||||||
|
tree: &SparseVoxelOctree,
|
||||||
|
) -> Mesh {
|
||||||
|
let mut positions = Vec::<[f32; 3]>::new();
|
||||||
|
let mut normals = Vec::<[f32; 3]>::new();
|
||||||
|
let mut uvs = Vec::<[f32; 2]>::new();
|
||||||
|
let mut indices = Vec::<u32>::new();
|
||||||
|
|
||||||
|
// helper – safe test for a filled voxel
|
||||||
|
let filled = |x: i32, y: i32, z: i32| -> bool {
|
||||||
|
if (0..CHUNK_SIZE).contains(&x)
|
||||||
|
&& (0..CHUNK_SIZE).contains(&y)
|
||||||
|
&& (0..CHUNK_SIZE).contains(&z)
|
||||||
|
{
|
||||||
|
buffer[x as usize][y as usize][z as usize].is_some()
|
||||||
|
} else {
|
||||||
|
let world = origin + Vec3::new(x as f32 * step,
|
||||||
|
y as f32 * step,
|
||||||
|
z as f32 * step);
|
||||||
|
tree.get_voxel_at_world_coords(world).is_some()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// push a single quad
|
||||||
|
let mut quad = |base: Vec3,
|
||||||
|
size: Vec2,
|
||||||
|
n: Vec3, // face normal (-1|+1 on one axis)
|
||||||
|
u: Vec3,
|
||||||
|
v: Vec3|
|
||||||
|
{
|
||||||
|
let i0 = positions.len() as u32;
|
||||||
|
|
||||||
|
// 4 vertices -----------------------------------------------------------
|
||||||
|
positions.extend_from_slice(&[
|
||||||
|
(base).into(),
|
||||||
|
(base + u * size.x).into(),
|
||||||
|
(base + u * size.x + v * size.y).into(),
|
||||||
|
(base + v * size.y).into(),
|
||||||
|
]);
|
||||||
|
normals.extend_from_slice(&[[n.x, n.y, n.z]; 4]);
|
||||||
|
uvs .extend_from_slice(&[[0.0,1.0],[1.0,1.0],[1.0,0.0],[0.0,0.0]]);
|
||||||
|
|
||||||
|
// indices -- flip for the negative-side faces -------------------------
|
||||||
|
if n.x + n.y + n.z >= 0.0 {
|
||||||
|
// CCW (front-face)
|
||||||
|
indices.extend_from_slice(&[i0, i0 + 1, i0 + 2, i0 + 2, i0 + 3, i0]);
|
||||||
|
} else {
|
||||||
|
// CW → reverse two vertices so that the winding becomes CCW again
|
||||||
|
indices.extend_from_slice(&[i0, i0 + 3, i0 + 2, i0 + 2, i0 + 1, i0]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------
|
||||||
|
// Z–faces
|
||||||
|
//-----------------------------------------------------------------------
|
||||||
|
for z in 0..CHUNK_SIZE { // -Z faces (normal −Z)
|
||||||
|
let nz = -1;
|
||||||
|
let voxel_z = z;
|
||||||
|
let neighbour_z = voxel_z as i32 + nz;
|
||||||
|
|
||||||
|
for y in 0..CHUNK_SIZE {
|
||||||
|
let mut x = 0;
|
||||||
|
while x < CHUNK_SIZE {
|
||||||
|
if filled(x, y, voxel_z) && !filled(x, y, neighbour_z) {
|
||||||
|
// greedy run along +X
|
||||||
|
let run_start = x;
|
||||||
|
let mut run = 1;
|
||||||
|
while x + run < CHUNK_SIZE
|
||||||
|
&& filled(x + run, y, voxel_z)
|
||||||
|
&& !filled(x + run, y, neighbour_z)
|
||||||
|
{
|
||||||
|
run += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let face_z = voxel_z as f32 * step + if nz == 1 { step } else { 0.0 };
|
||||||
|
let world_base = origin + Vec3::new(run_start as f32 * step, y as f32 * step, face_z);
|
||||||
|
|
||||||
|
quad(world_base,
|
||||||
|
Vec2::new(run as f32 * step, step),
|
||||||
|
Vec3::new(0.0, 0.0, nz as f32),
|
||||||
|
Vec3::X,
|
||||||
|
Vec3::Y);
|
||||||
|
|
||||||
|
x += run;
|
||||||
|
} else {
|
||||||
|
x += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------ 2nd pass : +Z faces ---------------------------------------------
|
||||||
|
for z in 0..CHUNK_SIZE { // +Z faces (normal +Z)
|
||||||
|
let nz = 1;
|
||||||
|
let voxel_z = z; // this voxel
|
||||||
|
let neighbour_z = voxel_z as i32 + nz; // cell “in front of it”
|
||||||
|
|
||||||
|
for y in 0..CHUNK_SIZE {
|
||||||
|
let mut x = 0;
|
||||||
|
while x < CHUNK_SIZE {
|
||||||
|
if filled(x, y, voxel_z) && !filled(x, y, neighbour_z) {
|
||||||
|
let run_start = x;
|
||||||
|
let mut run = 1;
|
||||||
|
while x + run < CHUNK_SIZE
|
||||||
|
&& filled(x + run, y, voxel_z)
|
||||||
|
&& !filled(x + run, y, neighbour_z)
|
||||||
|
{ run += 1; }
|
||||||
|
|
||||||
|
let world_base = origin
|
||||||
|
+ Vec3::new(run_start as f32 * step,
|
||||||
|
y as f32 * step,
|
||||||
|
(voxel_z + 1) as f32 * step); // +1 !
|
||||||
|
|
||||||
|
quad(world_base,
|
||||||
|
Vec2::new(run as f32 * step, step),
|
||||||
|
Vec3::new(0.0, 0.0, 1.0), // +Z
|
||||||
|
Vec3::X,
|
||||||
|
Vec3::Y);
|
||||||
|
|
||||||
|
x += run;
|
||||||
|
} else {
|
||||||
|
x += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
// X faces (-X pass … original code)
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
for x in 0..CHUNK_SIZE { // -X faces (normal −X)
|
||||||
|
let nx = -1;
|
||||||
|
let voxel_x = x;
|
||||||
|
let neighbour_x = voxel_x as i32 + nx;
|
||||||
|
|
||||||
|
for z in 0..CHUNK_SIZE {
|
||||||
|
let mut y = 0;
|
||||||
|
while y < CHUNK_SIZE {
|
||||||
|
if filled(voxel_x, y, z) && !filled(neighbour_x, y, z) {
|
||||||
|
let run_start = y;
|
||||||
|
let mut run = 1;
|
||||||
|
while y + run < CHUNK_SIZE
|
||||||
|
&& filled(voxel_x, y + run, z)
|
||||||
|
&& !filled(neighbour_x, y + run, z)
|
||||||
|
{ run += 1; }
|
||||||
|
|
||||||
|
// **fixed x-coordinate: add step when nx == +1**
|
||||||
|
let face_x = voxel_x as f32 * step + if nx == 1 { step } else { 0.0 };
|
||||||
|
|
||||||
|
let world_base = origin
|
||||||
|
+ Vec3::new(face_x,
|
||||||
|
run_start as f32 * step,
|
||||||
|
z as f32 * step);
|
||||||
|
|
||||||
|
quad(world_base,
|
||||||
|
Vec2::new(run as f32 * step, step),
|
||||||
|
Vec3::new(nx as f32, 0.0, 0.0),
|
||||||
|
Vec3::Y,
|
||||||
|
Vec3::Z);
|
||||||
|
|
||||||
|
y += run;
|
||||||
|
} else {
|
||||||
|
y += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------ 2nd pass : +X faces ---------------------------------------------
|
||||||
|
for x in 0..CHUNK_SIZE { // +X faces (normal +X)
|
||||||
|
let nx = 1;
|
||||||
|
let voxel_x = x;
|
||||||
|
let neighbour_x = voxel_x as i32 + nx;
|
||||||
|
|
||||||
|
for z in 0..CHUNK_SIZE {
|
||||||
|
let mut y = 0;
|
||||||
|
while y < CHUNK_SIZE {
|
||||||
|
if filled(voxel_x, y, z) && !filled(neighbour_x, y, z) {
|
||||||
|
let run_start = y;
|
||||||
|
let mut run = 1;
|
||||||
|
while y + run < CHUNK_SIZE
|
||||||
|
&& filled(voxel_x, y + run, z)
|
||||||
|
&& !filled(neighbour_x, y + run, z)
|
||||||
|
{ run += 1; }
|
||||||
|
|
||||||
|
let world_base = origin
|
||||||
|
+ Vec3::new((voxel_x + 1) as f32 * step, // +1 !
|
||||||
|
run_start as f32 * step,
|
||||||
|
z as f32 * step);
|
||||||
|
|
||||||
|
quad(world_base,
|
||||||
|
Vec2::new(run as f32 * step, step),
|
||||||
|
Vec3::new(1.0, 0.0, 0.0), // +X
|
||||||
|
Vec3::Y,
|
||||||
|
Vec3::Z);
|
||||||
|
|
||||||
|
y += run;
|
||||||
|
} else {
|
||||||
|
y += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Y faces (-Y pass … original code)
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
for y in 0..CHUNK_SIZE { // -Y faces (normal −Y)
|
||||||
|
let ny = -1;
|
||||||
|
let voxel_y = y;
|
||||||
|
let neighbour_y = voxel_y as i32 + ny;
|
||||||
|
|
||||||
|
for x in 0..CHUNK_SIZE {
|
||||||
|
let mut z = 0;
|
||||||
|
while z < CHUNK_SIZE {
|
||||||
|
if filled(x, voxel_y, z) && !filled(x, neighbour_y, z) {
|
||||||
|
let run_start = z;
|
||||||
|
let mut run = 1;
|
||||||
|
while z + run < CHUNK_SIZE
|
||||||
|
&& filled(x, voxel_y, z + run)
|
||||||
|
&& !filled(x, neighbour_y, z + run)
|
||||||
|
{ run += 1; }
|
||||||
|
|
||||||
|
// **fixed y-coordinate: add step when ny == +1**
|
||||||
|
let face_y = voxel_y as f32 * step + if ny == 1 { step } else { 0.0 };
|
||||||
|
|
||||||
|
let world_base = origin
|
||||||
|
+ Vec3::new(x as f32 * step,
|
||||||
|
face_y,
|
||||||
|
run_start as f32 * step);
|
||||||
|
|
||||||
|
quad(world_base,
|
||||||
|
Vec2::new(run as f32 * step, step),
|
||||||
|
Vec3::new(0.0, ny as f32, 0.0),
|
||||||
|
Vec3::Z,
|
||||||
|
Vec3::X);
|
||||||
|
|
||||||
|
z += run;
|
||||||
|
} else {
|
||||||
|
z += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ------ 2nd pass : +Y faces ---------------------------------------------
|
||||||
|
for y in 0..CHUNK_SIZE { // +Y faces (normal +Y)
|
||||||
|
let ny = 1;
|
||||||
|
let voxel_y = y;
|
||||||
|
let neighbour_y = voxel_y as i32 + ny;
|
||||||
|
|
||||||
|
for x in 0..CHUNK_SIZE {
|
||||||
|
let mut z = 0;
|
||||||
|
while z < CHUNK_SIZE {
|
||||||
|
if filled(x, voxel_y, z) && !filled(x, neighbour_y, z) {
|
||||||
|
let run_start = z;
|
||||||
|
let mut run = 1;
|
||||||
|
while z + run < CHUNK_SIZE
|
||||||
|
&& filled(x, voxel_y, z + run)
|
||||||
|
&& !filled(x, neighbour_y, z + run)
|
||||||
|
{ run += 1; }
|
||||||
|
|
||||||
|
let world_base = origin
|
||||||
|
+ Vec3::new(x as f32 * step,
|
||||||
|
(voxel_y + 1) as f32 * step, // +1 !
|
||||||
|
run_start as f32 * step);
|
||||||
|
|
||||||
|
quad(world_base,
|
||||||
|
Vec2::new(run as f32 * step, step),
|
||||||
|
Vec3::new(0.0, 1.0, 0.0), // +Y
|
||||||
|
Vec3::Z,
|
||||||
|
Vec3::X);
|
||||||
|
|
||||||
|
z += run;
|
||||||
|
} else {
|
||||||
|
z += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------
|
||||||
|
// build final mesh
|
||||||
|
//-----------------------------------------------------------------------
|
||||||
|
let mut mesh = Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::default());
|
||||||
|
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, VertexAttributeValues::Float32x3(positions));
|
||||||
|
mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, VertexAttributeValues::Float32x3(normals));
|
||||||
|
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, VertexAttributeValues::Float32x2(uvs));
|
||||||
|
mesh.insert_indices(Indices::U32(indices));
|
||||||
|
mesh
|
||||||
|
}*/
|
||||||
|
|
||||||
|
|
||||||
|
pub(crate) fn mesh_chunk(
|
||||||
|
buffer: &[[[Option<Voxel>; CHUNK_SIZE as usize]; CHUNK_SIZE as usize]; CHUNK_SIZE as usize],
|
||||||
|
origin: Vec3,
|
||||||
|
step: f32,
|
||||||
|
tree: &SparseVoxelOctree,
|
||||||
|
) -> Option<Mesh> {
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Helpers
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const N: usize = CHUNK_SIZE as usize;
|
||||||
|
const MASK_LEN: usize = N * N;
|
||||||
|
|
||||||
|
// Safe voxel query that falls back to the octree for out‑of‑chunk requests.
|
||||||
|
let filled = |x: i32, y: i32, z: i32| -> bool {
|
||||||
|
if (0..CHUNK_SIZE).contains(&x)
|
||||||
|
&& (0..CHUNK_SIZE).contains(&y)
|
||||||
|
&& (0..CHUNK_SIZE).contains(&z)
|
||||||
|
{
|
||||||
|
buffer[x as usize][y as usize][z as usize].is_some()
|
||||||
|
} else {
|
||||||
|
let world = origin + Vec3::new(x as f32 * step, y as f32 * step, z as f32 * step);
|
||||||
|
tree.get_voxel_at_world_coords(world).is_some()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Push a single quad (4 vertices, 6 indices). `base` is the lower‑left
|
||||||
|
// corner in world space; `u`/`v` are the tangent vectors (length 1); `size`
|
||||||
|
// is expressed in world units along those axes; `n` is the face normal.
|
||||||
|
// Preallocate vertex buffers for better performance
|
||||||
|
let voxel_count = N * N * N;
|
||||||
|
let mut positions = Vec::<[f32; 3]>::with_capacity(voxel_count * 4);
|
||||||
|
let mut normals = Vec::<[f32; 3]>::with_capacity(voxel_count * 4);
|
||||||
|
let mut uvs = Vec::<[f32; 2]>::with_capacity(voxel_count * 4);
|
||||||
|
let mut indices = Vec::<u32>::with_capacity(voxel_count * 6);
|
||||||
|
|
||||||
|
let mut push_quad = |base: Vec3, size: Vec2, n: Vec3, u: Vec3, v: Vec3| {
|
||||||
|
let i0 = positions.len() as u32;
|
||||||
|
positions.extend_from_slice(&[
|
||||||
|
(base).into(),
|
||||||
|
(base + u * size.x).into(),
|
||||||
|
(base + u * size.x + v * size.y).into(),
|
||||||
|
(base + v * size.y).into(),
|
||||||
|
]);
|
||||||
|
normals.extend_from_slice(&[[n.x, n.y, n.z]; 4]);
|
||||||
|
uvs.extend_from_slice(&[[0.0, 1.0], [1.0, 1.0], [1.0, 0.0], [0.0, 0.0]]);
|
||||||
|
|
||||||
|
if n.x + n.y + n.z >= 0.0 {
|
||||||
|
indices.extend_from_slice(&[i0, i0 + 1, i0 + 2, i0 + 2, i0 + 3, i0]);
|
||||||
|
} else {
|
||||||
|
// Flip winding for faces with a negative normal component sum so the
|
||||||
|
// result is still counter‑clockwise.
|
||||||
|
indices.extend_from_slice(&[i0, i0 + 3, i0 + 2, i0 + 2, i0 + 1, i0]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Greedy meshing
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Axes: 0→X, 1→Y, 2→Z. For each axis we process the negative and positive
|
||||||
|
// faces (dir = −1 / +1).
|
||||||
|
for (axis, dir) in [ (0, -1), (0, 1), (1, -1), (1, 1), (2, -1), (2, 1) ] {
|
||||||
|
// Mapping of (u,v) axes and their unit vectors in world space.
|
||||||
|
let (u_axis, v_axis, face_normal, u_vec, v_vec) = match (axis, dir) {
|
||||||
|
(0, d) => (1, 2, Vec3::new(d as f32, 0.0, 0.0), Vec3::Y, Vec3::Z),
|
||||||
|
(1, d) => (2, 0, Vec3::new(0.0, d as f32, 0.0), Vec3::Z, Vec3::X),
|
||||||
|
(2, d) => (0, 1, Vec3::new(0.0, 0.0, d as f32), Vec3::X, Vec3::Y),
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Iterate over every slice perpendicular to `axis`. Faces can lie on
|
||||||
|
// the 0…N grid lines (inclusive) because the positive‑side faces of the
|
||||||
|
// last voxel sit at slice N.
|
||||||
|
for slice in 0..=N {
|
||||||
|
// Build the face mask for this slice using a fixed-size array to
|
||||||
|
// avoid heap allocations.
|
||||||
|
let mut mask = [false; MASK_LEN];
|
||||||
|
let mut visited = [false; MASK_LEN];
|
||||||
|
let idx = |u: usize, v: usize| -> usize { u * N + v };
|
||||||
|
|
||||||
|
for u in 0..N {
|
||||||
|
for v in 0..N {
|
||||||
|
// Translate (u,v,slice) to (x,y,z) voxel coordinates.
|
||||||
|
let mut cell = [0i32; 3];
|
||||||
|
let mut neighbor = [0i32; 3];
|
||||||
|
|
||||||
|
cell [axis] = slice as i32 + if dir == 1 { -1 } else { 0 };
|
||||||
|
neighbor[axis] = cell[axis] + dir;
|
||||||
|
|
||||||
|
cell [u_axis] = u as i32;
|
||||||
|
cell [v_axis] = v as i32;
|
||||||
|
neighbor[u_axis] = u as i32;
|
||||||
|
neighbor[v_axis] = v as i32;
|
||||||
|
|
||||||
|
if filled(cell[0], cell[1], cell[2]) && !filled(neighbor[0], neighbor[1], neighbor[2]) {
|
||||||
|
mask[idx(u, v)] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Greedy merge the mask into maximal rectangles.
|
||||||
|
for u0 in 0..N {
|
||||||
|
for v0 in 0..N {
|
||||||
|
if !mask[idx(u0, v0)] || visited[idx(u0, v0)] {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the rectangle width.
|
||||||
|
let mut width = 1;
|
||||||
|
while u0 + width < N && mask[idx(u0 + width, v0)] && !visited[idx(u0 + width, v0)] {
|
||||||
|
width += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the rectangle height.
|
||||||
|
let mut height = 1;
|
||||||
|
'h: while v0 + height < N {
|
||||||
|
for du in 0..width {
|
||||||
|
if !mask[idx(u0 + du, v0 + height)] || visited[idx(u0 + du, v0 + height)] {
|
||||||
|
break 'h;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
height += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark the rectangle area as visited.
|
||||||
|
for du in 0..width {
|
||||||
|
for dv in 0..height {
|
||||||
|
visited[idx(u0 + du, v0 + dv)] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute world‑space base corner.
|
||||||
|
let mut base = origin;
|
||||||
|
match axis {
|
||||||
|
0 => {
|
||||||
|
base.x += step * slice as f32;
|
||||||
|
base.y += step * u0 as f32;
|
||||||
|
base.z += step * v0 as f32;
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
base.x += step * v0 as f32;
|
||||||
|
base.y += step * slice as f32;
|
||||||
|
base.z += step * u0 as f32;
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
base.x += step * u0 as f32;
|
||||||
|
base.y += step * v0 as f32;
|
||||||
|
base.z += step * slice as f32;
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
let size = Vec2::new(width as f32 * step, height as f32 * step);
|
||||||
|
push_quad(base, size, face_normal, u_vec, v_vec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Final mesh assembly
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
if indices.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut mesh = Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::default());
|
||||||
|
mesh.insert_attribute(
|
||||||
|
Mesh::ATTRIBUTE_POSITION,
|
||||||
|
VertexAttributeValues::Float32x3(positions),
|
||||||
|
);
|
||||||
|
mesh.insert_attribute(
|
||||||
|
Mesh::ATTRIBUTE_NORMAL,
|
||||||
|
VertexAttributeValues::Float32x3(normals),
|
||||||
|
);
|
||||||
|
mesh.insert_attribute(
|
||||||
|
Mesh::ATTRIBUTE_UV_0,
|
||||||
|
VertexAttributeValues::Float32x2(uvs),
|
||||||
|
);
|
||||||
|
mesh.insert_indices(Indices::U32(indices));
|
||||||
|
Some(mesh)
|
||||||
|
}
|
||||||
11
client/src/plugins/environment/systems/voxels/mod.rs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
pub mod debug;
|
||||||
|
pub mod helper;
|
||||||
|
pub mod octree;
|
||||||
|
pub mod structure;
|
||||||
|
|
||||||
|
mod chunk;
|
||||||
|
mod meshing;
|
||||||
|
pub mod render_chunks;
|
||||||
|
pub mod culling;
|
||||||
|
pub mod queue_systems;
|
||||||
|
pub mod lod;
|
||||||
464
client/src/plugins/environment/systems/voxels/octree.rs
Normal file
@ -0,0 +1,464 @@
|
|||||||
|
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::plugins::environment::systems::voxels::helper::chunk_key_from_world;
|
||||||
|
use crate::plugins::environment::systems::voxels::structure::{DirtyVoxel, OctreeNode, Ray, SparseVoxelOctree, Voxel, AABB, NEIGHBOR_OFFSETS, CHUNK_SIZE, ChunkKey};
|
||||||
|
|
||||||
|
impl SparseVoxelOctree {
|
||||||
|
/// Creates a new octree with the specified max depth, size, and wireframe visibility.
|
||||||
|
pub fn new(max_depth: u32, size: f32, 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: Vec::new(),
|
||||||
|
dirty_chunks: Default::default(),
|
||||||
|
occupied_chunks: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn insert(&mut self, position: Vec3, voxel: Voxel) {
|
||||||
|
// Align to the center of the voxel at max_depth
|
||||||
|
let mut aligned = self.normalize_to_voxel_at_depth(position, self.max_depth);
|
||||||
|
let mut world_center = self.denormalize_voxel_center(aligned);
|
||||||
|
|
||||||
|
// Expand as needed using the denormalized position.
|
||||||
|
while !self.contains(world_center.x, world_center.y, world_center.z) {
|
||||||
|
self.expand_root(world_center.x, world_center.y, world_center.z);
|
||||||
|
// Recompute aligned and world_center after expansion.
|
||||||
|
aligned = self.normalize_to_voxel_at_depth(position, self.max_depth);
|
||||||
|
world_center = self.denormalize_voxel_center(aligned);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dirty_voxel = DirtyVoxel{
|
||||||
|
position: aligned,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.dirty.push(dirty_voxel);
|
||||||
|
let key = chunk_key_from_world(self, position);
|
||||||
|
self.dirty_chunks.insert(key);
|
||||||
|
self.mark_neighbor_chunks_dirty(position);
|
||||||
|
self.occupied_chunks.insert(key);
|
||||||
|
|
||||||
|
|
||||||
|
Self::insert_recursive(&mut self.root, aligned, voxel, self.max_depth);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_recursive(node: &mut OctreeNode, position: Vec3, voxel: Voxel, depth: u32) {
|
||||||
|
if depth == 0 {
|
||||||
|
node.voxel = Some(voxel);
|
||||||
|
node.is_leaf = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let epsilon = 1e-6;
|
||||||
|
// Determine octant index by comparing with 0.5
|
||||||
|
let index = ((position.x >= 0.5 - epsilon) as usize)
|
||||||
|
+ ((position.y >= 0.5 - epsilon) as usize * 2)
|
||||||
|
+ ((position.z >= 0.5 - epsilon) as usize * 4);
|
||||||
|
|
||||||
|
// If there are no children, create them.
|
||||||
|
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 {
|
||||||
|
// Adjust coordinate into the child’s [0, 1] range.
|
||||||
|
let adjust_coord = |coord: f32| {
|
||||||
|
if coord >= 0.5 - epsilon {
|
||||||
|
(coord - 0.5) * 2.0
|
||||||
|
} else {
|
||||||
|
coord * 2.0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let child_pos = Vec3::new(
|
||||||
|
adjust_coord(position.x),
|
||||||
|
adjust_coord(position.y),
|
||||||
|
adjust_coord(position.z),
|
||||||
|
);
|
||||||
|
Self::insert_recursive(&mut children[index], child_pos, voxel, depth - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(&mut self, position: Vec3) {
|
||||||
|
let aligned = self.normalize_to_voxel_at_depth(position, self.max_depth);
|
||||||
|
|
||||||
|
self.dirty.push(DirtyVoxel { position: aligned });
|
||||||
|
|
||||||
|
// mark the chunk
|
||||||
|
let key = chunk_key_from_world(self, position);
|
||||||
|
self.dirty_chunks.insert(key);
|
||||||
|
self.mark_neighbor_chunks_dirty(position);
|
||||||
|
|
||||||
|
Self::remove_recursive(
|
||||||
|
&mut self.root,
|
||||||
|
aligned.x,
|
||||||
|
aligned.y,
|
||||||
|
aligned.z,
|
||||||
|
self.max_depth,
|
||||||
|
);
|
||||||
|
|
||||||
|
if !self.chunk_has_any_voxel(key) {
|
||||||
|
self.occupied_chunks.remove(&key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_dirty_flags(&mut self) {
|
||||||
|
self.dirty.clear();
|
||||||
|
self.dirty_chunks.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mark_neighbor_chunks_dirty(&mut self, position: Vec3) {
|
||||||
|
let key = chunk_key_from_world(self, position);
|
||||||
|
let step = self.get_spacing_at_depth(self.max_depth);
|
||||||
|
let half = self.size * 0.5;
|
||||||
|
|
||||||
|
let gx = ((position.x + half) / step).floor() as i32;
|
||||||
|
let gy = ((position.y + half) / step).floor() as i32;
|
||||||
|
let gz = ((position.z + half) / step).floor() as i32;
|
||||||
|
|
||||||
|
let lx = gx - key.0 * CHUNK_SIZE;
|
||||||
|
let ly = gy - key.1 * CHUNK_SIZE;
|
||||||
|
let lz = gz - key.2 * CHUNK_SIZE;
|
||||||
|
|
||||||
|
let mut neighbors = [
|
||||||
|
(lx == 0, ChunkKey(key.0 - 1, key.1, key.2)),
|
||||||
|
(lx == CHUNK_SIZE - 1, ChunkKey(key.0 + 1, key.1, key.2)),
|
||||||
|
(ly == 0, ChunkKey(key.0, key.1 - 1, key.2)),
|
||||||
|
(ly == CHUNK_SIZE - 1, ChunkKey(key.0, key.1 + 1, key.2)),
|
||||||
|
(lz == 0, ChunkKey(key.0, key.1, key.2 - 1)),
|
||||||
|
(lz == CHUNK_SIZE - 1, ChunkKey(key.0, key.1, key.2 + 1)),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (cond, n) in neighbors.iter() {
|
||||||
|
if *cond && self.occupied_chunks.contains(n) {
|
||||||
|
self.dirty_chunks.insert(*n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark all six neighbor chunks of the given key as dirty if they exist.
|
||||||
|
pub fn mark_neighbors_dirty_from_key(&mut self, key: ChunkKey) {
|
||||||
|
let offsets = [
|
||||||
|
(-1, 0, 0), (1, 0, 0),
|
||||||
|
(0, -1, 0), (0, 1, 0),
|
||||||
|
(0, 0, -1), (0, 0, 1),
|
||||||
|
];
|
||||||
|
for (dx, dy, dz) in offsets {
|
||||||
|
let neighbor = ChunkKey(key.0 + dx, key.1 + dy, key.2 + dz);
|
||||||
|
if self.occupied_chunks.contains(&neighbor) {
|
||||||
|
self.dirty_chunks.insert(neighbor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_recursive(
|
||||||
|
node: &mut OctreeNode,
|
||||||
|
x: f32,
|
||||||
|
y: f32,
|
||||||
|
z: f32,
|
||||||
|
depth: u32,
|
||||||
|
) -> bool {
|
||||||
|
if depth == 0 {
|
||||||
|
if node.voxel.is_some() {
|
||||||
|
node.voxel = None;
|
||||||
|
node.is_leaf = false;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.children.is_none() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
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: f32| {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
node.children = None;
|
||||||
|
node.is_leaf = true;
|
||||||
|
return node.voxel.is_none();
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn expand_root(&mut self, _x: f32, _y: f32, _z: f32) {
|
||||||
|
info!("Root expanding ...");
|
||||||
|
// Save the old root and its size.
|
||||||
|
let old_root = std::mem::replace(&mut self.root, OctreeNode::new());
|
||||||
|
let old_size = self.size;
|
||||||
|
|
||||||
|
// Update the octree's size and depth.
|
||||||
|
self.size *= 2.0;
|
||||||
|
self.max_depth += 1;
|
||||||
|
|
||||||
|
// Reinsert each voxel from the old tree.
|
||||||
|
let voxels = Self::collect_voxels_from_node(&old_root, old_size);
|
||||||
|
for (world_pos, voxel, _depth) in voxels {
|
||||||
|
self.insert(world_pos, voxel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper: Collect all voxels from a given octree node recursively.
|
||||||
|
/// The coordinate system here assumes the node covers [–old_size/2, +old_size/2] in each axis.
|
||||||
|
fn collect_voxels_from_node(node: &OctreeNode, old_size: f32) -> Vec<(Vec3, Voxel, u32)> {
|
||||||
|
let mut voxels = Vec::new();
|
||||||
|
Self::collect_voxels_recursive(node, -old_size / 2.0, -old_size / 2.0, -old_size / 2.0, old_size, 0, &mut voxels);
|
||||||
|
voxels
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_voxels_recursive(
|
||||||
|
node: &OctreeNode,
|
||||||
|
x: f32,
|
||||||
|
y: f32,
|
||||||
|
z: f32,
|
||||||
|
size: f32,
|
||||||
|
depth: u32,
|
||||||
|
out: &mut Vec<(Vec3, Voxel, u32)>,
|
||||||
|
) {
|
||||||
|
if node.is_leaf {
|
||||||
|
if let Some(voxel) = node.voxel {
|
||||||
|
// Compute the center of this node's region.
|
||||||
|
let center = Vec3::new(x + size / 2.0, y + size / 2.0, z + size / 2.0);
|
||||||
|
out.push((center, voxel, depth));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(children) = &node.children {
|
||||||
|
let half = size / 2.0;
|
||||||
|
for (i, child) in children.iter().enumerate() {
|
||||||
|
let offset_x = if (i & 1) != 0 { half } else { 0.0 };
|
||||||
|
let offset_y = if (i & 2) != 0 { half } else { 0.0 };
|
||||||
|
let offset_z = if (i & 4) != 0 { half } else { 0.0 };
|
||||||
|
Self::collect_voxels_recursive(child, x + offset_x, y + offset_y, z + offset_z, half, depth + 1, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
pub fn traverse(&self) -> Vec<(Vec3, Color, u32)> {
|
||||||
|
let mut voxels = Vec::new();
|
||||||
|
// Start at the normalized center (0.5, 0.5, 0.5) rather than (0,0,0)
|
||||||
|
Self::traverse_recursive(
|
||||||
|
&self.root,
|
||||||
|
Vec3::splat(0.5), // normalized center of the root cell
|
||||||
|
1.0, // full normalized cell size
|
||||||
|
0,
|
||||||
|
&mut voxels,
|
||||||
|
self,
|
||||||
|
);
|
||||||
|
voxels
|
||||||
|
}
|
||||||
|
|
||||||
|
fn traverse_recursive(
|
||||||
|
node: &OctreeNode,
|
||||||
|
local_center: Vec3,
|
||||||
|
size: f32,
|
||||||
|
depth: u32,
|
||||||
|
out: &mut Vec<(Vec3, Color, u32)>,
|
||||||
|
octree: &SparseVoxelOctree,
|
||||||
|
) {
|
||||||
|
// If a leaf contains a voxel, record its world-space center
|
||||||
|
if node.is_leaf {
|
||||||
|
if let Some(voxel) = node.voxel {
|
||||||
|
out.push((octree.denormalize_voxel_center(local_center), voxel.color, depth));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the node has children, subdivide the cell into 8 subcells.
|
||||||
|
if let Some(ref children) = node.children {
|
||||||
|
let offset = size / 4.0; // child center offset from parent center
|
||||||
|
let new_size = size / 2.0; // each child cell's size in normalized space
|
||||||
|
for (i, child) in children.iter().enumerate() {
|
||||||
|
// Compute each axis' offset: use +offset if the bit is set, else -offset.
|
||||||
|
let dx = if (i & 1) != 0 { offset } else { -offset };
|
||||||
|
let dy = if (i & 2) != 0 { offset } else { -offset };
|
||||||
|
let dz = if (i & 4) != 0 { offset } else { -offset };
|
||||||
|
let child_center = local_center + Vec3::new(dx, dy, dz);
|
||||||
|
|
||||||
|
Self::traverse_recursive(child, child_center, new_size, depth + 1, out, octree);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Retrieve a voxel from the octree if it exists (x,y,z in [-0.5..+0.5] range).
|
||||||
|
pub fn get_voxel_at(&self, x: f32, y: f32, z: f32) -> Option<&Voxel> {
|
||||||
|
Self::get_voxel_recursive(&self.root, x, y, z)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_voxel_recursive(node: &OctreeNode, x: f32, y: f32, z: f32) -> Option<&Voxel> {
|
||||||
|
if node.is_leaf {
|
||||||
|
return node.voxel.as_ref();
|
||||||
|
}
|
||||||
|
if let Some(children) = &node.children {
|
||||||
|
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: f32| {
|
||||||
|
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,
|
||||||
|
position: Vec3,
|
||||||
|
offset_x: i32,
|
||||||
|
offset_y: i32,
|
||||||
|
offset_z: i32,
|
||||||
|
depth: u32,
|
||||||
|
) -> bool {
|
||||||
|
let aligned = self.normalize_to_voxel_at_depth(position, depth);
|
||||||
|
let voxel_count = 2_u32.pow(depth) as f32;
|
||||||
|
// Normalized voxel size is 1/voxel_count
|
||||||
|
let norm_voxel_size = 1.0 / voxel_count;
|
||||||
|
|
||||||
|
let neighbor = Vec3::new(
|
||||||
|
aligned.x + (offset_x as f32) * norm_voxel_size,
|
||||||
|
aligned.y + (offset_y as f32) * norm_voxel_size,
|
||||||
|
aligned.z + (offset_z as f32) * norm_voxel_size,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Convert the normalized neighbor coordinate back to world space
|
||||||
|
let half_size = self.size * 0.5;
|
||||||
|
let neighbor_world = neighbor * self.size - Vec3::splat(half_size);
|
||||||
|
|
||||||
|
if !self.contains(neighbor_world.x, neighbor_world.y, neighbor_world.z) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.get_voxel_at_world_coords(neighbor_world).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Performs a raycast against the octree and returns the first intersected voxel.
|
||||||
|
pub fn raycast(&self, ray: &Ray) -> Option<(f32, f32, f32, 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<(f32, f32, f32, 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 f32,
|
||||||
|
hit_position.y as f32,
|
||||||
|
hit_position.z as f32,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
use bevy::prelude::*;
|
||||||
|
use crate::plugins::environment::systems::voxels::helper::world_to_chunk;
|
||||||
|
use crate::plugins::environment::systems::voxels::structure::*;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// enqueue chunks that *should* be visible but are not yet spawned
|
||||||
|
/// enqueue chunks that *should* be visible but are not yet spawned
|
||||||
|
pub fn enqueue_visible_chunks(
|
||||||
|
mut queue : ResMut<ChunkQueue>,
|
||||||
|
spawned : Res<SpawnedChunks>,
|
||||||
|
mut prev_cam : ResMut<PrevCameraChunk>,
|
||||||
|
cfg : Res<ChunkCullingCfg>,
|
||||||
|
cam_q : Query<&GlobalTransform, With<Camera>>,
|
||||||
|
tree_q : Query<&SparseVoxelOctree>,
|
||||||
|
) {
|
||||||
|
let tree = tree_q.single();
|
||||||
|
let cam_pos = cam_q.single().translation();
|
||||||
|
let centre = world_to_chunk(tree, cam_pos);
|
||||||
|
|
||||||
|
if prev_cam.0 == Some(centre) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
prev_cam.0 = Some(centre);
|
||||||
|
|
||||||
|
let r = cfg.view_distance_chunks;
|
||||||
|
for key in &tree.occupied_chunks {
|
||||||
|
let dx = key.0 - centre.0;
|
||||||
|
let dy = key.1 - centre.1;
|
||||||
|
let dz = key.2 - centre.2;
|
||||||
|
if dx.abs() > r || dy.abs() > r || dz.abs() > r { continue; }
|
||||||
|
if spawned.0.contains_key(key) { continue; }
|
||||||
|
if queue.set.contains(key) { continue; }
|
||||||
|
queue.keys.push_back(*key);
|
||||||
|
queue.set.insert(*key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// move a limited number of keys from the queue into the octree’s dirty set
|
||||||
|
pub fn process_chunk_queue(
|
||||||
|
mut queue : ResMut<ChunkQueue>,
|
||||||
|
budget : Res<ChunkBudget>,
|
||||||
|
mut tree_q : Query<&mut SparseVoxelOctree>,
|
||||||
|
) {
|
||||||
|
let mut tree = tree_q.single_mut();
|
||||||
|
for _ in 0..budget.per_frame {
|
||||||
|
if let Some(key) = queue.keys.pop_front() {
|
||||||
|
queue.set.remove(&key);
|
||||||
|
tree.dirty_chunks.insert(key);
|
||||||
|
} else { break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
125
client/src/plugins/environment/systems/voxels/render_chunks.rs
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt::format;
|
||||||
|
use bevy::pbr::wireframe::Wireframe;
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy::render::mesh::Mesh;
|
||||||
|
use big_space::prelude::GridCell;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use crate::plugins::big_space::big_space_plugin::RootGrid;
|
||||||
|
use crate::plugins::environment::systems::voxels::meshing::mesh_chunk;
|
||||||
|
use crate::plugins::environment::systems::voxels::structure::*;
|
||||||
|
|
||||||
|
/// rebuilds meshes only for chunks flagged dirty by the octree
|
||||||
|
pub fn rebuild_dirty_chunks(
|
||||||
|
mut commands : Commands,
|
||||||
|
mut octrees : Query<&mut SparseVoxelOctree>,
|
||||||
|
mut meshes : ResMut<Assets<Mesh>>,
|
||||||
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||||
|
chunk_q : Query<(Entity,
|
||||||
|
&Chunk,
|
||||||
|
&Mesh3d,
|
||||||
|
&MeshMaterial3d<StandardMaterial>,
|
||||||
|
&ChunkLod)>,
|
||||||
|
mut spawned : ResMut<SpawnedChunks>,
|
||||||
|
root : Res<RootGrid>,
|
||||||
|
) {
|
||||||
|
// map ChunkKey → (entity, mesh-handle, material-handle)
|
||||||
|
let existing: HashMap<ChunkKey, (Entity, Handle<Mesh>, Handle<StandardMaterial>, u32)> =
|
||||||
|
chunk_q
|
||||||
|
.iter()
|
||||||
|
.map(|(e, c, m, mat, lod)| (c.key, (e, m.0.clone(), mat.0.clone(), lod.0)))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for mut tree in &mut octrees {
|
||||||
|
if tree.dirty_chunks.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------ collect voxel data
|
||||||
|
let mut bufs = Vec::new();
|
||||||
|
for key in tree.dirty_chunks.iter().copied() {
|
||||||
|
let lod = existing.get(&key).map(|v| v.3).unwrap_or(0);
|
||||||
|
let mut buf =
|
||||||
|
[[[None; CHUNK_SIZE as usize]; CHUNK_SIZE as usize]; CHUNK_SIZE as usize];
|
||||||
|
|
||||||
|
let half = tree.size * 0.5;
|
||||||
|
let step = tree.get_spacing_at_depth(tree.max_depth);
|
||||||
|
let origin = Vec3::new(
|
||||||
|
key.0 as f32 * CHUNK_SIZE as f32 * step - half,
|
||||||
|
key.1 as f32 * CHUNK_SIZE as f32 * step - half,
|
||||||
|
key.2 as f32 * CHUNK_SIZE as f32 * step - half,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mult = 1 << lod;
|
||||||
|
for gx in (0..CHUNK_SIZE).step_by(mult as usize) {
|
||||||
|
for gy in (0..CHUNK_SIZE).step_by(mult as usize) {
|
||||||
|
for gz in (0..CHUNK_SIZE).step_by(mult as usize) {
|
||||||
|
let center = origin
|
||||||
|
+ Vec3::new(
|
||||||
|
(gx + mult / 2) as f32 * step,
|
||||||
|
(gy + mult / 2) as f32 * step,
|
||||||
|
(gz + mult / 2) as f32 * step,
|
||||||
|
);
|
||||||
|
if let Some(v) = tree.get_voxel_at_world_coords(center) {
|
||||||
|
for lx in 0..mult {
|
||||||
|
for ly in 0..mult {
|
||||||
|
for lz in 0..mult {
|
||||||
|
let ix = gx + lx;
|
||||||
|
let iy = gy + ly;
|
||||||
|
let iz = gz + lz;
|
||||||
|
if ix < CHUNK_SIZE && iy < CHUNK_SIZE && iz < CHUNK_SIZE {
|
||||||
|
buf[ix as usize][iy as usize][iz as usize] = Some(*v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bufs.push((key, buf, origin, step, lod));
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------ create / update
|
||||||
|
for (key, buf, origin, step, lod) in bufs {
|
||||||
|
if let Some((ent, mesh_h, _mat_h, _)) = existing.get(&key).cloned() {
|
||||||
|
// update mesh in-place; keeps old asset id
|
||||||
|
match mesh_chunk(&buf, origin, step, &tree) {
|
||||||
|
Some(new_mesh) => {
|
||||||
|
if let Some(mesh) = meshes.get_mut(&mesh_h) {
|
||||||
|
*mesh = new_mesh;
|
||||||
|
}
|
||||||
|
spawned.0.insert(key, ent);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
meshes.remove(&mesh_h);
|
||||||
|
commands.entity(ent).despawn_recursive();
|
||||||
|
spawned.0.remove(&key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let Some(mesh) = mesh_chunk(&buf, origin, step, &tree) {
|
||||||
|
// spawn brand-new chunk only if mesh has faces
|
||||||
|
let mesh_h = meshes.add(mesh);
|
||||||
|
let mat_h = materials.add(StandardMaterial::default());
|
||||||
|
|
||||||
|
commands.entity(root.0).with_children(|p| {
|
||||||
|
let e = p
|
||||||
|
.spawn((
|
||||||
|
Mesh3d::from(mesh_h.clone()),
|
||||||
|
MeshMaterial3d(mat_h.clone()),
|
||||||
|
Transform::default(),
|
||||||
|
GridCell::<i64>::ZERO,
|
||||||
|
Chunk { key, voxels: Vec::new(), dirty: false },
|
||||||
|
ChunkLod(lod),
|
||||||
|
/*Wireframe,*/
|
||||||
|
))
|
||||||
|
.id();
|
||||||
|
spawned.0.insert(key, e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tree.clear_dirty_flags();
|
||||||
|
}
|
||||||
|
}
|
||||||
154
client/src/plugins/environment/systems/voxels/structure.rs
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
use std::collections::{HashMap, HashSet, VecDeque};
|
||||||
|
use bevy::color::Color;
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
|
||||||
|
/// Represents a single voxel with a color.
|
||||||
|
#[derive(Debug, Clone, Copy, Component, PartialEq, Default)]
|
||||||
|
pub struct Voxel {
|
||||||
|
pub color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct DirtyVoxel {
|
||||||
|
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)]
|
||||||
|
pub struct SparseVoxelOctree {
|
||||||
|
|
||||||
|
pub root: OctreeNode,
|
||||||
|
pub max_depth: u32,
|
||||||
|
pub size: f32,
|
||||||
|
pub show_wireframe: bool,
|
||||||
|
pub show_world_grid: bool,
|
||||||
|
pub show_chunks: bool,
|
||||||
|
|
||||||
|
pub dirty: Vec<DirtyVoxel>,
|
||||||
|
pub dirty_chunks: HashSet<ChunkKey>,
|
||||||
|
pub occupied_chunks: HashSet<ChunkKey>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub const NEIGHBOR_OFFSETS: [(f32, f32, f32); 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
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Ray {
|
||||||
|
pub origin: Vec3,
|
||||||
|
pub direction: Vec3,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AABB {
|
||||||
|
pub min: Vec3,
|
||||||
|
pub max: Vec3,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const CHUNK_SIZE: i32 = 16; // 16×16×16 voxels
|
||||||
|
pub const CHUNK_POW : u32 = 4;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct Chunk {
|
||||||
|
pub key: ChunkKey,
|
||||||
|
pub voxels: Vec<(IVec3, Voxel)>, // local coords 0‥15
|
||||||
|
pub dirty: bool,
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component, Debug, Clone, Copy)]
|
||||||
|
pub struct ChunkLod(pub u32);
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||||
|
pub struct ChunkKey(pub i32, pub i32, pub i32);
|
||||||
|
|
||||||
|
|
||||||
|
/// maximum amount of *new* chunk meshes we are willing to create each frame
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct ChunkBudget {
|
||||||
|
pub per_frame: usize,
|
||||||
|
}
|
||||||
|
impl Default for ChunkBudget {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self { per_frame: 4 } // tweak to taste
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// FIFO queue with chunk keys that still need meshing
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
pub struct ChunkQueue {
|
||||||
|
pub keys: VecDeque<ChunkKey>,
|
||||||
|
pub set: HashSet<ChunkKey>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// map “which chunk key already has an entity in the world?”
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
pub struct SpawnedChunks(pub HashMap<ChunkKey, Entity>);
|
||||||
|
|
||||||
|
/// how big the cube around the player is, measured in chunks
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct ChunkCullingCfg { pub view_distance_chunks: i32 }
|
||||||
|
impl Default for ChunkCullingCfg { fn default() -> Self { Self { view_distance_chunks: 6 } } }
|
||||||
|
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
pub struct PrevCameraChunk(pub Option<ChunkKey>);
|
||||||
|
|
||||||
|
#[derive(Resource, Clone)]
|
||||||
|
pub struct ChunkOffsets(pub Vec<IVec3>);
|
||||||
|
|
||||||
|
impl ChunkOffsets {
|
||||||
|
pub fn new(radius: i32) -> Self {
|
||||||
|
let mut offsets = Vec::new();
|
||||||
|
for dx in -radius..=radius {
|
||||||
|
for dy in -radius..=radius {
|
||||||
|
for dz in -radius..=radius {
|
||||||
|
offsets.push(IVec3::new(dx, dy, dz));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offsets.sort_by_key(|v| v.x * v.x + v.y * v.y + v.z * v.z);
|
||||||
|
Self(offsets)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,6 @@
|
|||||||
|
|
||||||
use bevy::app::{App, Plugin, PreUpdate, Startup};
|
use bevy::app::{App, Plugin, PreUpdate, Startup};
|
||||||
use bevy::prelude::{IntoSystemConfigs, Update};
|
use bevy::prelude::{IntoSystemConfigs, Update};
|
||||||
use crate::plugins::input::systems::console::{console_system, toggle_console, ConsoleState};
|
|
||||||
|
|
||||||
pub struct InputPlugin;
|
pub struct InputPlugin;
|
||||||
impl Plugin for InputPlugin {
|
impl Plugin for InputPlugin {
|
||||||
@ -9,17 +8,14 @@ impl Plugin for InputPlugin {
|
|||||||
_app.add_systems(
|
_app.add_systems(
|
||||||
Update,
|
Update,
|
||||||
(
|
(
|
||||||
crate::plugins::input::systems::console::console_system,
|
|
||||||
crate::plugins::input::systems::flight::flight_systems,
|
crate::plugins::input::systems::flight::flight_systems,
|
||||||
crate::plugins::input::systems::ui::ui_system,
|
crate::plugins::input::systems::ui::ui_system,
|
||||||
//crate::plugins::input::systems::network::network_system,
|
//crate::plugins::input::systems::network::network_system,
|
||||||
crate::plugins::input::systems::movement::movement_system,
|
crate::plugins::input::systems::movement::movement_system,
|
||||||
|
crate::plugins::input::systems::voxels::voxel_system
|
||||||
|
|
||||||
),
|
),
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
_app.insert_resource(ConsoleState::default());
|
|
||||||
_app.add_systems(Update, (toggle_console, console_system));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,63 +0,0 @@
|
|||||||
use bevy::app::AppExit;
|
|
||||||
use bevy::input::ButtonInput;
|
|
||||||
use bevy::input::mouse::{MouseMotion, MouseWheel};
|
|
||||||
use bevy::prelude::{EventReader, EventWriter, KeyCode, Query, Res, ResMut, Resource, Time, Transform};
|
|
||||||
use bevy_egui::{egui, EguiContexts};
|
|
||||||
use bevy_window::Window;
|
|
||||||
use crate::plugins::environment::systems::camera_system::CameraController;
|
|
||||||
use crate::plugins::network::systems::database::DbConnectionResource;
|
|
||||||
|
|
||||||
pub fn console_system(
|
|
||||||
mut ctxs: EguiContexts,
|
|
||||||
mut state: ResMut<ConsoleState>,
|
|
||||||
) {
|
|
||||||
if !state.open { return; }
|
|
||||||
|
|
||||||
egui::Window::new("Console")
|
|
||||||
.resizable(true)
|
|
||||||
.vscroll(true)
|
|
||||||
.show(ctxs.ctx_mut(), |ui| {
|
|
||||||
// Output
|
|
||||||
for line in &state.output {
|
|
||||||
ui.label(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Input line
|
|
||||||
let resp = ui.text_edit_singleline(&mut state.input);
|
|
||||||
if resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
|
||||||
let cmd = state.input.trim().to_string();
|
|
||||||
if !cmd.is_empty() {
|
|
||||||
state.history.push(cmd.clone());
|
|
||||||
handle_command(&cmd, &mut state.output);
|
|
||||||
state.input.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
/// Press ` to open / close
|
|
||||||
pub fn toggle_console(
|
|
||||||
mut state: ResMut<ConsoleState>,
|
|
||||||
keys: Res<ButtonInput<KeyCode>>,
|
|
||||||
) {
|
|
||||||
if keys.just_pressed(KeyCode::KeyC) {
|
|
||||||
state.open = !state.open;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add your own commands here.
|
|
||||||
/// For demo purposes we just echo the input.
|
|
||||||
fn handle_command(cmd: &str, out: &mut Vec<String>) {
|
|
||||||
match cmd.trim() {
|
|
||||||
"help" => out.push("Available: help, clear, echo …".into()),
|
|
||||||
"clear" => out.clear(),
|
|
||||||
_ => out.push(format!("> {cmd}")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Resource, Default)]
|
|
||||||
pub struct ConsoleState {
|
|
||||||
pub open: bool,
|
|
||||||
pub input: String,
|
|
||||||
pub history: Vec<String>,
|
|
||||||
pub output: Vec<String>,
|
|
||||||
}
|
|
||||||
@ -2,111 +2,85 @@ use bevy::app::AppExit;
|
|||||||
use bevy::input::ButtonInput;
|
use bevy::input::ButtonInput;
|
||||||
use bevy::input::mouse::{MouseMotion, MouseWheel};
|
use bevy::input::mouse::{MouseMotion, MouseWheel};
|
||||||
use bevy::math::{Quat, Vec3};
|
use bevy::math::{Quat, Vec3};
|
||||||
use bevy::prelude::{EventReader, EventWriter, KeyCode, Query, Res, ResMut, Time, Transform};
|
use bevy::prelude::*;
|
||||||
use bevy_window::{CursorGrabMode, Window};
|
|
||||||
use random_word::Lang;
|
|
||||||
use spacetimedb_sdk::DbContext;
|
|
||||||
use crate::module_bindings::{set_name, set_position, spawn_entity, DbTransform, DbVector3, DbVector4, PlayerTableAccess};
|
|
||||||
use crate::plugins::environment::systems::camera_system::CameraController;
|
use crate::plugins::environment::systems::camera_system::CameraController;
|
||||||
use crate::plugins::network::systems::database::DbConnectionResource;
|
|
||||||
|
|
||||||
|
fn move_by(
|
||||||
|
mut q: Query<&mut Transform, With<CameraController>>,
|
||||||
|
delta: Vec3,
|
||||||
|
) {
|
||||||
|
for mut t in &mut q {
|
||||||
|
t.translation += delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Example system to input a camera using double-precision for position.
|
/// Example system to input a camera using double-precision for position.
|
||||||
pub fn flight_systems(
|
pub fn flight_systems(
|
||||||
time: Res<Time>,
|
time: Res<Time>,
|
||||||
keyboard_input: Res<ButtonInput<KeyCode>>, /*
|
keyboard: Res<ButtonInput<KeyCode>>,
|
||||||
mouse_button_input: Res<ButtonInput<MouseButton>>,*/
|
mut mouse_motion: EventReader<MouseMotion>,
|
||||||
mut mouse_motion_events: EventReader<MouseMotion>,
|
mut mouse_wheel: EventReader<MouseWheel>,
|
||||||
mut mouse_wheel_events: EventReader<MouseWheel>,
|
|
||||||
mut windows: Query<&mut Window>,
|
mut windows: Query<&mut Window>,
|
||||||
mut query: Query<(&mut Transform, &mut CameraController)>,
|
// all camera entities carry this tag
|
||||||
mut app_exit_events: EventWriter<AppExit>,
|
mut xforms: Query<&mut Transform, With<CameraController>>,
|
||||||
//mut ctx: ResMut<DbConnectionResource>,
|
mut ctrls: Query<&mut CameraController>,
|
||||||
|
mut exit_ev: EventWriter<AppExit>,
|
||||||
) {
|
) {
|
||||||
|
//------------------------------------------------------------
|
||||||
|
// 0) Early-out if no camera
|
||||||
|
//------------------------------------------------------------
|
||||||
|
if xforms.is_empty() { return; }
|
||||||
|
|
||||||
|
//------------------------------------------------------------
|
||||||
|
// 1) Rotation & speed input (borrow transform/controller)
|
||||||
|
//------------------------------------------------------------
|
||||||
|
let delta_vec3 = {
|
||||||
let mut window = windows.single_mut();
|
let mut window = windows.single_mut();
|
||||||
let (mut transform, mut controller) = query.single_mut();
|
let mut transform = xforms.single_mut();
|
||||||
|
let mut controller = ctrls.single_mut();
|
||||||
|
|
||||||
// ====================
|
//------------------ mouse look --------------------------
|
||||||
// 1) Handle Mouse Look
|
|
||||||
// ====================
|
|
||||||
if !window.cursor_options.visible {
|
if !window.cursor_options.visible {
|
||||||
for event in mouse_motion_events.read() {
|
for ev in mouse_motion.read() {
|
||||||
// Adjust yaw/pitch in f32
|
controller.yaw -= ev.delta.x * controller.sensitivity;
|
||||||
controller.yaw -= event.delta.x * controller.sensitivity;
|
controller.pitch += ev.delta.y * controller.sensitivity;
|
||||||
controller.pitch += event.delta.y * controller.sensitivity;
|
|
||||||
controller.pitch = controller.pitch.clamp(-89.9, 89.9);
|
controller.pitch = controller.pitch.clamp(-89.9, 89.9);
|
||||||
|
|
||||||
// Convert degrees to radians (f32)
|
let yaw = controller.yaw.to_radians();
|
||||||
let yaw_radians = controller.yaw.to_radians();
|
let pitch = controller.pitch.to_radians();
|
||||||
let pitch_radians = controller.pitch.to_radians();
|
|
||||||
|
|
||||||
// Build a double-precision quaternion from those angles
|
let rot = Quat::from_rotation_y(yaw) * Quat::from_rotation_x(-pitch);
|
||||||
let rot_yaw = Quat::from_axis_angle(Vec3::Y, yaw_radians);
|
transform.rotation = rot;
|
||||||
let rot_pitch = Quat::from_axis_angle(Vec3::X, -pitch_radians);
|
|
||||||
|
|
||||||
transform.rotation = rot_yaw * rot_pitch;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====================
|
//------------------ mouse wheel speed -------------------
|
||||||
// 2) Adjust Movement Speed with Mouse Wheel
|
for ev in mouse_wheel.read() {
|
||||||
// ====================
|
controller.speed = (controller.speed * 1.1_f32.powf(ev.y)).max(0.01);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//------------------ keyboard direction -----------------
|
||||||
|
let mut dir = Vec3::ZERO;
|
||||||
|
if keyboard.pressed(KeyCode::KeyW) { dir += *transform.forward(); }
|
||||||
|
if keyboard.pressed(KeyCode::KeyS) { dir -= *transform.forward(); }
|
||||||
|
if keyboard.pressed(KeyCode::KeyA) { dir -= *transform.right(); }
|
||||||
|
if keyboard.pressed(KeyCode::KeyD) { dir += *transform.right(); }
|
||||||
|
if keyboard.pressed(KeyCode::Space) { dir += *transform.up(); }
|
||||||
|
if keyboard.pressed(KeyCode::ShiftLeft) ||
|
||||||
|
keyboard.pressed(KeyCode::ShiftRight) { dir -= *transform.up(); }
|
||||||
|
|
||||||
// ====================
|
if dir.length_squared() > 0.0 { dir = dir.normalize(); }
|
||||||
// 3) Handle Keyboard Movement (WASD, Space, Shift)
|
|
||||||
// ====================
|
|
||||||
let mut direction = Vec3::ZERO;
|
|
||||||
|
|
||||||
// Forward/Back
|
|
||||||
if keyboard_input.pressed(KeyCode::KeyW) {
|
|
||||||
direction += transform.forward().as_vec3();
|
|
||||||
}
|
|
||||||
if keyboard_input.pressed(KeyCode::KeyS) {
|
|
||||||
direction -= transform.forward().as_vec3();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Left/Right
|
|
||||||
if keyboard_input.pressed(KeyCode::KeyA) {
|
|
||||||
direction -= transform.right().as_vec3();
|
|
||||||
}
|
|
||||||
if keyboard_input.pressed(KeyCode::KeyD) {
|
|
||||||
direction += transform.right().as_vec3();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Up/Down
|
|
||||||
if keyboard_input.pressed(KeyCode::Space) {
|
|
||||||
direction += transform.up().as_vec3();
|
|
||||||
}
|
|
||||||
if keyboard_input.pressed(KeyCode::ShiftLeft) || keyboard_input.pressed(KeyCode::ShiftRight) {
|
|
||||||
direction -= transform.up().as_vec3();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
transform.translation += direction * distance as f32;
|
|
||||||
|
|
||||||
/*ctx.0.reducers.set_position(DbVector3{
|
|
||||||
x: transform.translation.x,
|
|
||||||
y: transform.translation.y,
|
|
||||||
z: transform.translation.z,
|
|
||||||
}).expect("TODO: panic message");
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
//------------------ compute delta ----------------------
|
||||||
|
let distance = controller.speed * time.delta_secs_f64() as f32;
|
||||||
|
dir * distance
|
||||||
|
}; // ⬅ scopes end here; mutable borrows are dropped
|
||||||
|
|
||||||
|
//------------------------------------------------------------
|
||||||
|
// 2) Apply translation with the helper
|
||||||
|
//------------------------------------------------------------
|
||||||
|
move_by(xforms, delta_vec3);
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -1,5 +1,4 @@
|
|||||||
pub mod movement;
|
pub mod movement;
|
||||||
pub mod flight;
|
pub mod flight;
|
||||||
pub mod console;
|
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
pub mod network;
|
pub mod voxels;
|
||||||
@ -1,51 +0,0 @@
|
|||||||
|
|
||||||
use bevy::input::ButtonInput;
|
|
||||||
use bevy::math::{EulerRot, Quat};
|
|
||||||
use bevy::prelude::{KeyCode, Res, ResMut,};
|
|
||||||
use random_word::Lang;
|
|
||||||
use crate::module_bindings::{set_name, spawn_entity, spawn_rigidbody_entity, DbTransform, DbVector3, DbVector4, EntityType};
|
|
||||||
use crate::plugins::network::systems::database::DbConnectionResource;
|
|
||||||
|
|
||||||
pub fn network_system(
|
|
||||||
keyboard_input: Res<ButtonInput<KeyCode>>,
|
|
||||||
ctx: ResMut<DbConnectionResource>,
|
|
||||||
) {
|
|
||||||
|
|
||||||
|
|
||||||
if keyboard_input.just_pressed(KeyCode::KeyQ) {
|
|
||||||
let word = random_word::get(Lang::En);
|
|
||||||
|
|
||||||
ctx.0.reducers.set_name(word.to_string()).unwrap();
|
|
||||||
}
|
|
||||||
if keyboard_input.just_pressed(KeyCode::KeyE) {
|
|
||||||
let rand_position = crate::helper::vector_helper::random_vec3(-10.0, 10.0);
|
|
||||||
let rand_rotation = crate::helper::vector_helper::random_vec3(0.0, 10.0);
|
|
||||||
let rand_rotation = Quat::from_euler(EulerRot::XYZ,rand_rotation.x,rand_rotation.y,rand_rotation.z).normalize();
|
|
||||||
let rand_scale = crate::helper::vector_helper::random_vec3(0.1, 1.0);
|
|
||||||
ctx.0.reducers.spawn_rigidbody_entity(DbTransform{
|
|
||||||
position: DbVector3{
|
|
||||||
x: rand_position.x,
|
|
||||||
y: rand_position.y,
|
|
||||||
z: rand_position.z,
|
|
||||||
},
|
|
||||||
rotation: DbVector4 {
|
|
||||||
x: rand_rotation.x,
|
|
||||||
y: rand_rotation.y,
|
|
||||||
z: rand_rotation.z,
|
|
||||||
w: rand_rotation.w,
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
scale: DbVector3 {
|
|
||||||
x: rand_scale.x,
|
|
||||||
y: rand_scale.x,
|
|
||||||
z: rand_scale.x,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
EntityType::Cube,
|
|
||||||
DbVector3{ x:0.0, y:0.0, z:0.0}, 5.0, false).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
use bevy::app::AppExit;
|
use bevy::app::AppExit;
|
||||||
use bevy::input::ButtonInput;
|
use bevy::input::ButtonInput;
|
||||||
use bevy::prelude::{EventWriter, KeyCode, Query, Res,};
|
use bevy::prelude::*;
|
||||||
use bevy_window::{CursorGrabMode, Window};
|
use bevy::window::CursorGrabMode;
|
||||||
|
|
||||||
pub fn ui_system(
|
pub fn ui_system(
|
||||||
keyboard_input: Res<ButtonInput<KeyCode>>,
|
keyboard_input: Res<ButtonInput<KeyCode>>,
|
||||||
|
|||||||
99
client/src/plugins/input/systems/voxels.rs
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
use bevy::prelude::*;
|
||||||
|
use crate::plugins::environment::systems::camera_system::CameraController;
|
||||||
|
use crate::plugins::environment::systems::voxels::structure::*;
|
||||||
|
|
||||||
|
///TODO
|
||||||
|
pub fn voxel_system(
|
||||||
|
|
||||||
|
keyboard_input: Res<ButtonInput<KeyCode>>,
|
||||||
|
mouse_button_input: Res<ButtonInput<MouseButton>>,
|
||||||
|
mut octree_query: Query<&mut SparseVoxelOctree>,
|
||||||
|
|
||||||
|
mut query: Query<(&mut Transform, &mut CameraController)>,
|
||||||
|
mut windows: Query<&mut Window>,
|
||||||
|
) {
|
||||||
|
let mut window = windows.single_mut();
|
||||||
|
let (mut transform, _) = query.single_mut();
|
||||||
|
|
||||||
|
// =======================
|
||||||
|
// 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(transform.translation, 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 = transform.translation;
|
||||||
|
let ray_direction = transform.forward().normalize();
|
||||||
|
|
||||||
|
let ray = Ray {
|
||||||
|
origin: ray_origin,
|
||||||
|
direction: ray_direction,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
for mut octree in octree_query.iter_mut() {
|
||||||
|
if let Some((hit_x, hit_y, hit_z, depth,normal)) = octree.raycast(&ray) {
|
||||||
|
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
// Remove the voxel
|
||||||
|
octree.remove(offset_position);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
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));
|
||||||
|
|
||||||
|
// Insert the new voxel
|
||||||
|
octree.insert(
|
||||||
|
offset_position,
|
||||||
|
Voxel::new(Color::srgb(1.0, 0.0, 0.0)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
pub mod environment;
|
pub mod environment;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
pub mod network;
|
|
||||||
pub mod input;
|
pub mod input;
|
||||||
|
pub(crate) mod big_space;
|
||||||
|
|||||||
@ -1,2 +0,0 @@
|
|||||||
pub(crate) mod systems;
|
|
||||||
pub mod network_plugin;
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
use bevy::app::{App, Plugin, Startup};
|
|
||||||
use bevy::color::palettes::basic::{GREEN, YELLOW};
|
|
||||||
use bevy::color::palettes::css::RED;
|
|
||||||
use bevy::prelude::*;
|
|
||||||
use crate::plugins::environment::systems::environment_system::*;
|
|
||||||
use crate::plugins::network::systems::database::setup_database;
|
|
||||||
use crate::plugins::network::systems::entities::*;
|
|
||||||
|
|
||||||
pub struct NetworkPlugin;
|
|
||||||
impl Plugin for NetworkPlugin {
|
|
||||||
fn build(&self, app: &mut App) {
|
|
||||||
app.add_systems(PreStartup, setup_database);
|
|
||||||
app.add_systems(PostUpdate, sync_entities_system);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
use bevy::log::{error, info};
|
|
||||||
use spacetimedb_sdk::Status;
|
|
||||||
use crate::module_bindings::{EventContext, Player, ReducerEventContext, RemoteDbContext};
|
|
||||||
|
|
||||||
/// Our `User::on_insert` callback:
|
|
||||||
/// if the user is online, print a notification.
|
|
||||||
pub fn on_user_inserted(_ctx: &EventContext, user: &Player) {
|
|
||||||
if user.online {
|
|
||||||
info!("User {} connected.", user_name_or_identity(user));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn user_name_or_identity(user: &Player) -> String {
|
|
||||||
user.name
|
|
||||||
.clone()
|
|
||||||
.unwrap_or_else(|| user.identity.to_hex().to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Our `User::on_update` callback:
|
|
||||||
/// print a notification about name and status changes.
|
|
||||||
pub fn on_user_updated(_ctx: &EventContext, old: &Player, new: &Player) {
|
|
||||||
if old.name != new.name {
|
|
||||||
info!(
|
|
||||||
"User {} renamed to {}.",
|
|
||||||
user_name_or_identity(old),
|
|
||||||
user_name_or_identity(new)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if old.online && !new.online {
|
|
||||||
info!("User {} disconnected.", user_name_or_identity(new));
|
|
||||||
}
|
|
||||||
if !old.online && new.online {
|
|
||||||
info!("User {} connected.", user_name_or_identity(new));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Our `on_set_name` callback: print a warning if the reducer failed.
|
|
||||||
pub fn on_name_set(ctx: &ReducerEventContext, name: &String) {
|
|
||||||
if let Status::Failed(err) = &ctx.event.status {
|
|
||||||
error!("Failed to change name to {:?}: {}", name, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Our `on_send_message` callback: print a warning if the reducer failed.
|
|
||||||
pub fn on_message_sent(ctx: &ReducerEventContext, text: &String) {
|
|
||||||
if let Status::Failed(err) = &ctx.event.status {
|
|
||||||
error!("Failed to send message {:?}: {}", text, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
use spacetimedb_sdk::{credentials, Error, Identity};
|
|
||||||
use crate::module_bindings::{DbConnection, ErrorContext};
|
|
||||||
|
|
||||||
|
|
||||||
pub fn creds_store() -> credentials::File {
|
|
||||||
credentials::File::new("token")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Our `on_connect` callback: save our credentials to a file.
|
|
||||||
pub fn on_connected(_ctx: &DbConnection, _identity: Identity, token: &str) {
|
|
||||||
if let Err(e) = creds_store().save(token) {
|
|
||||||
eprintln!("Failed to save credentials: {:?}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Our `on_connect_error` callback: print the error, then exit the process.
|
|
||||||
pub fn on_connect_error(_ctx: &ErrorContext, err: Error) {
|
|
||||||
eprintln!("Connection error: {:?}", err);
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Our `on_disconnect` callback: print a note, then exit the process.
|
|
||||||
pub fn on_disconnected(_ctx: &ErrorContext, err: Option<Error>) {
|
|
||||||
if let Some(err) = err {
|
|
||||||
eprintln!("Disconnected: {}", err);
|
|
||||||
std::process::exit(1);
|
|
||||||
} else {
|
|
||||||
println!("Disconnected.");
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
use std::fmt::Debug;
|
|
||||||
use std::ops::Deref;
|
|
||||||
use bevy::ecs::system::SystemState;
|
|
||||||
use bevy::prelude::{Commands, DetectChanges, Mut, Res, ResMut, Resource, World};
|
|
||||||
use bevy::utils::info;
|
|
||||||
use spacetimedb_sdk::{credentials, DbContext, Error, Event, Identity, Status, Table, TableWithPrimaryKey};
|
|
||||||
use crate::config::ServerConfig;
|
|
||||||
use crate::module_bindings::*;
|
|
||||||
use crate::plugins::network::systems::callbacks::*;
|
|
||||||
use crate::plugins::network::systems::connection::*;
|
|
||||||
use crate::plugins::network::systems::subscriptions::*;
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Resource)]
|
|
||||||
pub struct DbConnectionResource(pub(crate) DbConnection);
|
|
||||||
|
|
||||||
pub fn setup_database(mut commands: Commands, config: Res<crate::Config>) {
|
|
||||||
// Call your connection function and insert the connection as a resource.
|
|
||||||
let ctx = connect_to_db(config);
|
|
||||||
register_callbacks(&ctx);
|
|
||||||
subscribe_to_tables(&ctx);
|
|
||||||
ctx.run_threaded();
|
|
||||||
commands.insert_resource(DbConnectionResource(ctx));
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Register subscriptions for all rows of both tables
|
|
||||||
|
|
||||||
fn connect_to_db(config: Res<crate::Config>) -> DbConnection {
|
|
||||||
|
|
||||||
println!("It's there: {:?}", &config.server);
|
|
||||||
|
|
||||||
DbConnection::builder()
|
|
||||||
.on_connect(on_connected)
|
|
||||||
.on_connect_error(on_connect_error)
|
|
||||||
.on_disconnect( on_disconnected)
|
|
||||||
.with_module_name(&config.server.database)
|
|
||||||
.with_uri(&config.server.host)
|
|
||||||
.build()
|
|
||||||
.expect("Failed to connect")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// Register all the callbacks our app will use to respond to database events.
|
|
||||||
fn register_callbacks(ctx: &DbConnection) {
|
|
||||||
// When a new user joins, print a notification.
|
|
||||||
ctx.db.player().on_insert(on_user_inserted);
|
|
||||||
|
|
||||||
// When a user's status changes, print a notification.
|
|
||||||
ctx.db.player().on_update(on_user_updated);
|
|
||||||
|
|
||||||
// When we fail to set our name, print a warning.
|
|
||||||
ctx.reducers.on_set_name(on_name_set);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn subscribe_to_tables(ctx: &DbConnection) {
|
|
||||||
ctx.subscription_builder()
|
|
||||||
.on_applied(on_sub_applied)
|
|
||||||
.on_error(on_sub_error)
|
|
||||||
.subscribe(["SELECT * FROM player", "SELECT * FROM entity", "SELECT * FROM rigidbody"]);
|
|
||||||
}
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
use std::collections::HashSet;
|
|
||||||
use bevy::log::debug;
|
|
||||||
use bevy::math::{NormedVectorSpace, Vec3};
|
|
||||||
use bevy::pbr::{MeshMaterial3d, StandardMaterial};
|
|
||||||
use bevy::prelude::{default, info, Bundle, Commands, Component, Cuboid, DespawnRecursiveExt, DetectChangesMut, Entity, GlobalTransform, Mesh, PbrBundle, Quat, Query, Res, ResMut, Sphere, Transform, TransformBundle};
|
|
||||||
use bevy_asset::Assets;
|
|
||||||
use bevy_reflect::Reflect;
|
|
||||||
use bevy_render::mesh::Mesh3d;
|
|
||||||
use spacetimedb_sdk::{DbContext, Table};
|
|
||||||
use crate::helper::math::RoundTo;
|
|
||||||
use crate::module_bindings::{DbTransform, DbVector3, EntityTableAccess, EntityType, PlayerTableAccess};
|
|
||||||
use crate::plugins::network::systems::database::DbConnectionResource;
|
|
||||||
|
|
||||||
#[derive(Component)]
|
|
||||||
pub struct EntityDto {
|
|
||||||
|
|
||||||
pub entity_id: u32,
|
|
||||||
|
|
||||||
|
|
||||||
pub transform: DbTransform,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<crate::module_bindings::Entity> for EntityDto {
|
|
||||||
fn from(e: crate::module_bindings::Entity) -> Self {
|
|
||||||
EntityDto {
|
|
||||||
entity_id: e.entity_id,
|
|
||||||
transform: e.transform,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// System that syncs DB entities with the Bevy ECS
|
|
||||||
pub fn sync_entities_system(
|
|
||||||
mut commands: Commands,
|
|
||||||
db_resource: Res<DbConnectionResource>,
|
|
||||||
|
|
||||||
// We need the Entity handle for potential despawning,
|
|
||||||
// plus mutable references if we want to update Transform/EntityDto
|
|
||||||
mut query: Query<(Entity, &mut Transform, &mut GlobalTransform, &mut EntityDto)>,
|
|
||||||
|
|
||||||
mut meshes: ResMut<Assets<Mesh>>,
|
|
||||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
|
||||||
) {
|
|
||||||
|
|
||||||
let identity = db_resource.0.identity();
|
|
||||||
let player = db_resource.0.db.player().identity().find(&identity);
|
|
||||||
|
|
||||||
// --- 1) Collect DB entities and build a set of IDs ---
|
|
||||||
let db_entities = db_resource.0.db.entity();
|
|
||||||
let db_ids: HashSet<u32> = db_entities.iter().map(|e| e.entity_id).collect();
|
|
||||||
|
|
||||||
// --- 2) For each DB entity, update or spawn in ECS ---
|
|
||||||
for db_entity in db_entities.iter() {
|
|
||||||
|
|
||||||
if db_entity.entity_id == player.clone().unwrap().entity_id {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to find a matching ECS entity by entity_id
|
|
||||||
if let Some((_, mut transform, mut global, mut dto)) =
|
|
||||||
query.iter_mut().find(|(_, _, _, dto)| dto.entity_id == db_entity.entity_id)
|
|
||||||
{
|
|
||||||
// Update fields
|
|
||||||
|
|
||||||
// build the new local Transform
|
|
||||||
let new_tf = Transform::from(db_entity.transform.clone());
|
|
||||||
|
|
||||||
// overwrite both components
|
|
||||||
*transform = new_tf;
|
|
||||||
*global = GlobalTransform::from(new_tf);
|
|
||||||
|
|
||||||
// keep your DTO in sync
|
|
||||||
dto.transform = db_entity.transform.clone();
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// Not found in ECS, so spawn a new entity
|
|
||||||
let debug_material = materials.add(StandardMaterial {
|
|
||||||
// fill out any fields you want
|
|
||||||
..default()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Pick a mesh based on the entity type
|
|
||||||
let entity_type = match db_entity.entity_type {
|
|
||||||
EntityType::Sphere => Mesh3d(meshes.add(Sphere::default())),
|
|
||||||
EntityType::Cube => Mesh3d(meshes.add(Cuboid::default())),
|
|
||||||
EntityType::Custom => todo!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let new_tf = Transform::from(db_entity.transform.clone());
|
|
||||||
|
|
||||||
commands.spawn((
|
|
||||||
TransformBundle::from_transform(new_tf), // inserts BOTH Transform and GlobalTransform
|
|
||||||
entity_type,
|
|
||||||
MeshMaterial3d(debug_material),
|
|
||||||
EntityDto::from(db_entity.clone()),
|
|
||||||
));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 3) Despawn any ECS entity that doesn't exist in the DB anymore ---
|
|
||||||
for (entity,_, _, dto) in query.iter_mut() {
|
|
||||||
if !db_ids.contains(&dto.entity_id) {
|
|
||||||
// This ECS entity no longer matches anything in the DB => remove it
|
|
||||||
commands.entity(entity).despawn_recursive();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
pub mod database;
|
|
||||||
mod connection;
|
|
||||||
mod callbacks;
|
|
||||||
mod subscriptions;
|
|
||||||
pub mod entities;
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
use bevy::prelude::info;
|
|
||||||
use spacetimedb_sdk::{Error, Table};
|
|
||||||
use crate::module_bindings::{ErrorContext, PlayerTableAccess, SubscriptionEventContext};
|
|
||||||
|
|
||||||
/// Our `on_subscription_applied` callback:
|
|
||||||
/// sort all past messages and print them in timestamp order.
|
|
||||||
pub fn on_sub_applied(ctx: &SubscriptionEventContext) {
|
|
||||||
|
|
||||||
let mut players = ctx.db.player().iter().collect::<Vec<_>>();
|
|
||||||
players.sort_by_key(|p| p.name.clone());
|
|
||||||
for player in players {
|
|
||||||
println!("Player {:?} online", player.name);
|
|
||||||
}
|
|
||||||
println!("Fully connected and all subscriptions applied.");
|
|
||||||
println!("Use /name to set your name, or type a message!");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Or `on_error` callback:
|
|
||||||
/// print the error, then exit the process.
|
|
||||||
pub fn on_sub_error(_ctx: &ErrorContext, err: Error) {
|
|
||||||
eprintln!("Subscription failed: {}", err);
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
@ -1,7 +1,10 @@
|
|||||||
use crate::plugins::environment::systems::camera_system::CameraController;
|
use crate::plugins::environment::systems::camera_system::CameraController;
|
||||||
use bevy::asset::AssetServer;
|
use bevy::asset::AssetServer;
|
||||||
|
use bevy::math::DVec3;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
use big_space::prelude::*;
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub struct SpeedDisplay;
|
pub struct SpeedDisplay;
|
||||||
|
|
||||||
@ -38,25 +41,26 @@ pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|||||||
/// - camera f32 position
|
/// - camera f32 position
|
||||||
/// - camera global f64 position
|
/// - camera global f64 position
|
||||||
/// - current chunk coordinate
|
/// - current chunk coordinate
|
||||||
|
|
||||||
pub fn update(
|
pub fn update(
|
||||||
// Query the camera controller so we can see its speed
|
grids: Grids<'_, '_, i64>, // helper from big_space
|
||||||
query_camera_controller: Query<&CameraController>,
|
// we need the entity id, the cell & the local transform
|
||||||
// We also query for the camera's f32 `Transform` and the double `DoubleTransform`
|
camera_q: Query<(Entity, &GridCell<i64>, &Transform, &CameraController)>,
|
||||||
camera_query: Query<(&Transform, &Camera)>,
|
mut ui_q: Query<&mut Text, With<SpeedDisplay>>,
|
||||||
|
|
||||||
// The UI text entity
|
|
||||||
mut query_text: Query<&mut Text, With<SpeedDisplay>>,
|
|
||||||
) {
|
) {
|
||||||
let camera_controller = query_camera_controller.single();
|
let Ok((cam_ent, cell, tf, ctrl)) = camera_q.get_single() else { return };
|
||||||
let (transform, _camera) = camera_query.single();
|
|
||||||
let mut text = query_text.single_mut();
|
|
||||||
|
|
||||||
// Format the string to show speed, positions, and chunk coords
|
// grid that the camera lives in
|
||||||
|
let Some(grid) = grids.parent_grid(cam_ent) else { return };
|
||||||
|
|
||||||
|
// absolute position in metres (f64)
|
||||||
|
let pos = grid.grid_position_double(cell,tf);
|
||||||
|
|
||||||
|
if let Ok(mut text) = ui_q.get_single_mut() {
|
||||||
text.0 = format!(
|
text.0 = format!(
|
||||||
"\n Speed: {:.3}\n Position(f32): ({:.2},{:.2},{:.2})",
|
"\n Speed: {:.3}\n Position(f64): ({:.2}, {:.2}, {:.2})",
|
||||||
camera_controller.speed,
|
ctrl.speed,
|
||||||
transform.translation.x,
|
pos.x, pos.y, pos.z,
|
||||||
transform.translation.y,
|
|
||||||
transform.translation.z,
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1,13 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "spacetime-module"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
crate-type = ["cdylib"]
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
spacetimedb = "1.0.1"
|
|
||||||
log = "0.4"
|
|
||||||
@ -1,103 +0,0 @@
|
|||||||
mod types;
|
|
||||||
|
|
||||||
use std::time::Duration;
|
|
||||||
use spacetimedb::{reducer, ReducerContext, ScheduleAt, Table, TimeDuration, Timestamp};
|
|
||||||
use crate::types::entity::{entity, entity__TableHandle, Entity, EntityType};
|
|
||||||
use crate::types::player::{player, player__TableHandle, Player};
|
|
||||||
use crate::types::rigidbody::physics_step;
|
|
||||||
use crate::types::types::{DBVector4, DbTransform, DbVector3};
|
|
||||||
|
|
||||||
#[spacetimedb::table(name = config, public)]
|
|
||||||
pub struct Config {
|
|
||||||
#[primary_key]
|
|
||||||
pub id: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[spacetimedb::reducer]
|
|
||||||
pub fn test(ctx: &ReducerContext) -> Result<(), String> {
|
|
||||||
log::debug!("This reducer was called by {}.", ctx.sender);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[reducer(client_connected)]
|
|
||||||
// Called when a client connects to the SpacetimeDB
|
|
||||||
pub fn client_connected(ctx: &ReducerContext) {
|
|
||||||
if let Some(player) = ctx.db.player().identity().find(ctx.sender) {
|
|
||||||
// If this is a returning player, i.e. we already have a `player` with this `Identity`,
|
|
||||||
// set `online: true`, but leave `name` and `identity` unchanged.
|
|
||||||
ctx.db.player().identity().update(Player { online: true, ..player });
|
|
||||||
} else {
|
|
||||||
// If this is a new player, create a `player` row for the `Identity`,
|
|
||||||
// which is online, but hasn't set a name.
|
|
||||||
let entity = ctx.db.entity().try_insert(Entity{
|
|
||||||
entity_id: 0,
|
|
||||||
transform: DbTransform{
|
|
||||||
position: DbVector3{x: 0.0, y: 0.0, z: 10.0},
|
|
||||||
rotation: DBVector4{x: 0.0, y: 0.0, z: 0.0, w: 1.0},
|
|
||||||
scale: DbVector3{x: 1.0, y: 1.0, z: 1.0},
|
|
||||||
},
|
|
||||||
|
|
||||||
entity_type: EntityType::Sphere,
|
|
||||||
}).expect("TODO: panic message");
|
|
||||||
|
|
||||||
ctx.db.player().insert(Player {
|
|
||||||
name: None,
|
|
||||||
identity: ctx.sender,
|
|
||||||
online: true,
|
|
||||||
entity_id: entity.entity_id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[reducer(client_disconnected)]
|
|
||||||
// Called when a client disconnects from SpacetimeDB
|
|
||||||
pub fn identity_disconnected(ctx: &ReducerContext) {
|
|
||||||
|
|
||||||
let entity: &entity__TableHandle = ctx.db.entity();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if let Some(player_iter) = ctx.db.player().identity().find(ctx.sender) {
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ctx.db.player().identity().update(Player { online: false, ..player_iter });
|
|
||||||
ctx.db.entity().iter().find(|e| e.entity_id == player_iter.entity_id).iter().for_each(|e| {
|
|
||||||
entity.delete(e.clone());
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// This branch should be unreachable,
|
|
||||||
// as it doesn't make sense for a client to disconnect without connecting first.
|
|
||||||
log::warn!("Disconnect event for unknown Player with identity {:?}", ctx.sender);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[spacetimedb::reducer(init)]
|
|
||||||
pub fn init(ctx: &ReducerContext) -> Result<(), String> {
|
|
||||||
log::info!("Initializing...");
|
|
||||||
ctx.db.config().try_insert(Config {
|
|
||||||
id: 0,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
|
|
||||||
ctx.db.physics_timer().try_insert(PhysicsTimer {
|
|
||||||
scheduled_id: 0,
|
|
||||||
scheduled_at: ScheduleAt::Interval(Duration::from_millis(50).into()),
|
|
||||||
last_update_ts: ctx.timestamp,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[spacetimedb::table(name = physics_timer, scheduled(physics_step))]
|
|
||||||
struct PhysicsTimer {
|
|
||||||
#[primary_key]
|
|
||||||
#[auto_inc]
|
|
||||||
scheduled_id: u64,
|
|
||||||
scheduled_at: ScheduleAt,
|
|
||||||
last_update_ts: Timestamp,
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
|
|
||||||
use spacetimedb::{Identity, ReducerContext, SpacetimeType, Table};
|
|
||||||
use crate::types::types::{DbVector3, DbTransform, DBVector4};
|
|
||||||
|
|
||||||
#[spacetimedb::table(name = entity, public)]
|
|
||||||
#[derive(Debug, Clone, )]
|
|
||||||
pub struct Entity {
|
|
||||||
#[auto_inc]
|
|
||||||
#[primary_key]
|
|
||||||
pub entity_id: u32,
|
|
||||||
pub transform: DbTransform,
|
|
||||||
pub entity_type: EntityType,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(SpacetimeType, Clone, Debug)]
|
|
||||||
pub enum EntityType {
|
|
||||||
Cube,
|
|
||||||
Sphere,
|
|
||||||
Custom
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[spacetimedb::reducer]
|
|
||||||
pub fn spawn_entity(ctx: &ReducerContext, transform: DbTransform) -> Result<(), String> {
|
|
||||||
|
|
||||||
ctx.db.entity().try_insert(Entity {
|
|
||||||
entity_id: 0,
|
|
||||||
transform,
|
|
||||||
entity_type: EntityType::Cube,
|
|
||||||
}).expect("TODO: panic message");
|
|
||||||
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
pub mod types;
|
|
||||||
pub mod player;
|
|
||||||
pub mod entity;
|
|
||||||
pub mod rigidbody;
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
use crate::types::entity::*;
|
|
||||||
use std::io::empty;
|
|
||||||
use spacetimedb::{reducer, Identity, ReducerContext, Table};
|
|
||||||
use crate::types::types::{DbTransform, DbVector3};
|
|
||||||
|
|
||||||
#[spacetimedb::table(name = player, public)]
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Player {
|
|
||||||
#[primary_key]
|
|
||||||
pub identity: Identity,
|
|
||||||
|
|
||||||
#[index(btree)]
|
|
||||||
pub entity_id: u32,
|
|
||||||
|
|
||||||
pub name: Option<String>,
|
|
||||||
pub online: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[reducer]
|
|
||||||
/// Clients invoke this reducer to set their user names.
|
|
||||||
pub fn set_name(ctx: &ReducerContext, name: String) -> Result<(), String> {
|
|
||||||
let name = validate_name(name)?;
|
|
||||||
if let Some(user) = ctx.db.player().identity().find(ctx.sender) {
|
|
||||||
ctx.db.player().identity().update(Player { name: Some(name), ..user });
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err("Cannot set name for unknown user".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[reducer]
|
|
||||||
/// Clients invoke this reducer to set their user names.
|
|
||||||
pub fn set_position(ctx: &ReducerContext, position: DbVector3) -> Result<(), String> {
|
|
||||||
if let Some(entity) = ctx.db.entity().iter().find(|e| e.entity_id == ctx.db.player().identity().find(ctx.sender).unwrap().entity_id) {
|
|
||||||
ctx.db.entity().entity_id()
|
|
||||||
.update(Entity{
|
|
||||||
transform: DbTransform{
|
|
||||||
position: position,
|
|
||||||
..entity.transform
|
|
||||||
},
|
|
||||||
..entity
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Takes a name and checks if it's acceptable as a user's name.
|
|
||||||
fn validate_name(name: String) -> Result<String, String> {
|
|
||||||
if name.is_empty() {
|
|
||||||
Err("Names must not be empty".to_string())
|
|
||||||
} else {
|
|
||||||
Ok(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,103 +0,0 @@
|
|||||||
use std::time::Duration;
|
|
||||||
use spacetimedb::{ReducerContext, Table, TimeDuration, Timestamp};
|
|
||||||
use crate::{physics_timer, PhysicsTimer};
|
|
||||||
use crate::types::entity::{entity, Entity, EntityType};
|
|
||||||
use crate::types::types::{DbTransform, DbVector3};
|
|
||||||
|
|
||||||
#[spacetimedb::table(name = rigidbody, public)]
|
|
||||||
#[derive(Debug, Clone, )]
|
|
||||||
pub struct Rigidbody {
|
|
||||||
#[auto_inc]
|
|
||||||
#[primary_key]
|
|
||||||
pub rigidbody_id: u32,
|
|
||||||
|
|
||||||
#[index(btree)]
|
|
||||||
pub entity_id: u32,
|
|
||||||
|
|
||||||
|
|
||||||
pub velocity: DbVector3,
|
|
||||||
pub force: DbVector3,
|
|
||||||
pub mass: f32,
|
|
||||||
|
|
||||||
pub is_fixed: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[spacetimedb::reducer]
|
|
||||||
pub fn spawn_rigidbody_entity(
|
|
||||||
ctx: &ReducerContext,
|
|
||||||
transform: DbTransform,
|
|
||||||
entity_type: EntityType,
|
|
||||||
velocity: DbVector3,
|
|
||||||
mass: f32,
|
|
||||||
is_fixed: bool,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
// 1. insert a new Entity row
|
|
||||||
let inserted_entity: Entity = ctx
|
|
||||||
.db
|
|
||||||
.entity()
|
|
||||||
.insert(Entity {
|
|
||||||
entity_id: 0,
|
|
||||||
transform,
|
|
||||||
entity_type,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2. insert its corresponding Rigidbody row
|
|
||||||
ctx.db
|
|
||||||
.rigidbody()
|
|
||||||
.insert(Rigidbody {
|
|
||||||
rigidbody_id: 0,
|
|
||||||
entity_id: inserted_entity.entity_id,
|
|
||||||
velocity,
|
|
||||||
force: DbVector3::zero(),
|
|
||||||
mass,
|
|
||||||
is_fixed,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[spacetimedb::reducer]
|
|
||||||
pub fn physics_step(ctx: &ReducerContext, mut timer: PhysicsTimer) -> Result<(), String> {
|
|
||||||
let now = ctx.timestamp;
|
|
||||||
let delta = now
|
|
||||||
.time_duration_since(timer.last_update_ts)
|
|
||||||
.unwrap_or(TimeDuration::from(Duration::from_millis(50)));
|
|
||||||
let dt = delta.to_duration().unwrap().as_secs_f32();
|
|
||||||
|
|
||||||
// update timer state
|
|
||||||
timer.last_update_ts = now;
|
|
||||||
ctx.db.physics_timer().scheduled_id().update(timer);
|
|
||||||
|
|
||||||
// constant gravity
|
|
||||||
let gravity = DbVector3::new(0.0, -9.81, 0.0);
|
|
||||||
|
|
||||||
// process all rigidbodies
|
|
||||||
let bodies: Vec<Rigidbody> = ctx.db.rigidbody().iter().collect();
|
|
||||||
for mut rb in bodies {
|
|
||||||
if rb.is_fixed {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// apply gravity to force
|
|
||||||
rb.force.add(&gravity.mul_scalar(rb.mass));
|
|
||||||
|
|
||||||
// integrate velocity
|
|
||||||
let inv_mass = 1.0 / rb.mass;
|
|
||||||
let accel = rb.force.mul_scalar(inv_mass);
|
|
||||||
rb.velocity.add(&accel.mul_scalar(dt));
|
|
||||||
|
|
||||||
// update corresponding entity position
|
|
||||||
if let Some(mut ent) = ctx.db.entity().iter().find(|e| e.entity_id == rb.entity_id){
|
|
||||||
ent.transform.position.add(&rb.velocity.mul_scalar(dt));
|
|
||||||
ctx.db.entity().entity_id().update(ent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// reset force and write back
|
|
||||||
rb.force = DbVector3::zero();
|
|
||||||
ctx.db.rigidbody().rigidbody_id().update(rb);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
use spacetimedb::SpacetimeType;
|
|
||||||
|
|
||||||
#[derive(SpacetimeType, Clone, Debug)]
|
|
||||||
pub struct DbVector3 {
|
|
||||||
pub x: f32,
|
|
||||||
pub y: f32,
|
|
||||||
pub z: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DbVector3 {
|
|
||||||
pub(crate) fn new(x: f32, y: f32, z: f32) -> DbVector3 {
|
|
||||||
DbVector3 { x, y, z }
|
|
||||||
}
|
|
||||||
pub(crate) fn zero() -> DbVector3 {
|
|
||||||
DbVector3::new(0.0, 0.0, 0.0)
|
|
||||||
}
|
|
||||||
pub(crate) fn add(&mut self, other: &DbVector3) {
|
|
||||||
self.x += other.x;
|
|
||||||
self.y += other.y;
|
|
||||||
self.z += other.z;
|
|
||||||
}
|
|
||||||
pub(crate) fn mul_scalar(&self, s: f32) -> DbVector3 {
|
|
||||||
DbVector3::new(self.x * s, self.y * s, self.z * s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[derive(SpacetimeType, Clone, Debug)]
|
|
||||||
pub struct DBVector4 {
|
|
||||||
pub x: f32,
|
|
||||||
pub y: f32,
|
|
||||||
pub z: f32,
|
|
||||||
pub w: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(SpacetimeType, Clone, Debug)]
|
|
||||||
pub struct DbTransform {
|
|
||||||
pub position: DbVector3,
|
|
||||||
pub rotation: DBVector4,
|
|
||||||
pub scale: DbVector3,
|
|
||||||
}
|
|
||||||
33
trim.bat
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
@echo off
|
||||||
|
rem combine_all.bat – merge every *.rs and *.toml in this tree
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
|
rem Output files
|
||||||
|
set "OUT_RS=target/combined.rs.out"
|
||||||
|
set "OUT_TOML=target/combined.toml.out"
|
||||||
|
|
||||||
|
if exist "%OUT_RS%" del "%OUT_RS%"
|
||||||
|
if exist "%OUT_TOML%" del "%OUT_TOML%"
|
||||||
|
|
||||||
|
rem -------- merge .rs --------
|
||||||
|
for /f "delims=" %%F in ('
|
||||||
|
dir /b /s /o:n *.rs ^| findstr /v /i "\\target\\"
|
||||||
|
') do (
|
||||||
|
echo /* --- %%~F --- */>>"%OUT_RS%"
|
||||||
|
type "%%F" >>"%OUT_RS%"
|
||||||
|
echo.>>"%OUT_RS%"
|
||||||
|
)
|
||||||
|
|
||||||
|
rem ----- merge .toml -----
|
||||||
|
for /f "delims=" %%F in ('
|
||||||
|
dir /b /s /o:n *.toml ^| findstr /v /i "\\target\\"
|
||||||
|
') do (
|
||||||
|
rem TOML uses # for comments
|
||||||
|
echo # --- %%~F --- >>"%OUT_TOML%"
|
||||||
|
type "%%F" >>"%OUT_TOML%"
|
||||||
|
echo.>>"%OUT_TOML%"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo Merged .rs files into %OUT_RS%
|
||||||
|
echo Merged .toml files into %OUT_TOML%
|
||||||
|
endlocal
|
||||||
@ -1,2 +0,0 @@
|
|||||||
|
|
||||||
watch spacetime logs network-game
|
|
||||||