commit addbd730e7649e8de317e81a0366da2586d8e2fa Author: Albin Chaboissier Date: Sun Jan 11 19:01:50 2026 +0100 Start clean diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..478e9e7 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "cache" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = "1.0.100" +bytemuck = "1.24.0" +crevice = {version = "0.18.0", features = ["cgmath"]} +egui = "0.33.3" +egui-wgpu = "0.33.3" +egui-winit = "0.33.3" +env_logger = "0.11.8" +pollster = "0.4.0" +wgpu = {version = "27.0.1", features = ["spirv"]} +winit = "0.30.12" +gpu_shared = {path = "gpu_shared/"} +glam = {version = "0.30.10", features = ["bytemuck"]} +itertools = "0.14.0" + +[build-dependencies] +spirv-builder = {git = "https://github.com/rust-gpu/rust-gpu"} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..06deb6d --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,7 @@ +[toolchain] +channel = "nightly-2025-06-30" +components = ["rust-src", "rustc-dev", "llvm-tools"] +# commit_hash = 35f6036521777bdc0dcea1f980be4c192962a168 + +# Whenever changing the nightly channel, update the commit hash above, and +# change `REQUIRED_RUST_TOOLCHAIN` in `crates/rustc_codegen_spirv/build.rs` too. diff --git a/src/egui_renderer.rs b/src/egui_renderer.rs new file mode 100644 index 0000000..ccb37ee --- /dev/null +++ b/src/egui_renderer.rs @@ -0,0 +1,137 @@ +use egui::{Context, PaintCallbackInfo}; +use egui_wgpu::{CallbackResources, CallbackTrait, Renderer, RendererOptions, ScreenDescriptor}; +use egui_winit::State; +use wgpu::{CommandEncoder, Device, Queue, RenderPass, TextureFormat, TextureView}; +use winit::{event::WindowEvent, window::Window}; + +pub struct EguiState { + pub state: State, + pub renderer: Renderer, + pub frame_started: bool, + //pub msaa_texture: TextureView, + pub color_format: TextureFormat, +} + +impl EguiState { + pub fn context(&self) -> &Context { + self.state.egui_ctx() + } + + pub fn new(device: &Device, output_color_format: TextureFormat, window: &Window) -> EguiState { + let egui_context = Context::default(); + + let egui_state = egui_winit::State::new( + egui_context, + egui::ViewportId::ROOT, + &window, + Some(window.scale_factor() as f32), + None, + Some(2 * 1024), + ); + + let options = RendererOptions { + //msaa_samples: SAMPLE_COUNT, + //depth_stencil_format: Some(TextureFormat::Depth24PlusStencil8), + ..Default::default() + }; + let egui_renderer = Renderer::new(device, output_color_format, options); + + EguiState { + state: egui_state, + renderer: egui_renderer, + frame_started: false, + color_format: output_color_format, + } + } + + pub fn resize(&mut self, _device: &Device, _width: u32, _height: u32) {} + + pub fn handle_event(&mut self, window: &Window, event: &WindowEvent) { + let _ = self.state.on_window_event(window, event); + } + + pub fn begin_frame(&mut self, window: &Window) { + let input = self.state.take_egui_input(window); + self.state.egui_ctx().begin_pass(input); + self.frame_started = true; + } + + pub fn end_frame_and_draw( + &mut self, + device: &Device, + queue: &Queue, + encoder: &mut CommandEncoder, + window: &Window, + window_surface_view: &TextureView, + screen_descriptor: ScreenDescriptor, + ) { + if !self.frame_started { + panic!("begin_frame must be called before end_frame_and_draw can be called!"); + } + + //self.ppp(screen_descriptor.pixels_per_point); + + let full_output = self.state.egui_ctx().end_pass(); + + self.state + .handle_platform_output(window, full_output.platform_output); + + let tris = self + .state + .egui_ctx() + .tessellate(full_output.shapes, self.state.egui_ctx().pixels_per_point()); + for (id, image_delta) in &full_output.textures_delta.set { + self.renderer + .update_texture(device, queue, *id, image_delta); + } + self.renderer + .update_buffers(device, queue, encoder, &tris, &screen_descriptor); + let rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: window_surface_view, + resolve_target: None, + ops: egui_wgpu::wgpu::Operations { + load: egui_wgpu::wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + depth_slice: None, + })], + depth_stencil_attachment: None, + label: Some("egui main render pass"), + timestamp_writes: None, + occlusion_query_set: None, + }); + + self.renderer + .render(&mut rpass.forget_lifetime(), &tris, &screen_descriptor); + for x in &full_output.textures_delta.free { + self.renderer.free_texture(x) + } + + self.frame_started = false; + } +} + +pub struct CallbackFn

+where + P: Fn(PaintCallbackInfo, &mut RenderPass<'static>, &CallbackResources), +{ + //pub : FnOnce(&Device, &Queue, &ScreenDescriptor, &mut CommandEncoder, &mut CallbackResources) -> Vec<> + pub paint_fn: P, +} + +impl

CallbackTrait for CallbackFn

+where + P: Fn(PaintCallbackInfo, &mut RenderPass<'static>, &CallbackResources) + + std::marker::Sync + + std::marker::Send, +{ + fn paint( + &self, + info: egui::PaintCallbackInfo, + render_pass: &mut wgpu::RenderPass<'static>, + callback_resources: &egui_wgpu::CallbackResources, + ) { + (self.paint_fn)(info, render_pass, callback_resources) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..7155981 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,119 @@ +#![allow(incomplete_features)] +#![feature(generic_const_exprs)] + +pub mod egui_renderer; +pub mod state; +pub mod voxel; + +use std::sync::Arc; + +use winit::application::ApplicationHandler; +use winit::event::WindowEvent; +use winit::event_loop::EventLoop; +use winit::event_loop::{self}; +use winit::window::Window; + +use crate::state::State; + +pub fn run() -> anyhow::Result<()> +{ + env_logger::init(); + + let event_loop = EventLoop::with_user_event().build()?; + let mut app = App::default(); + event_loop.run_app(&mut app)?; + + Ok(()) +} + +// App struct +#[derive(Default)] +pub struct App +{ + state: Option, +} + +impl ApplicationHandler for App +{ + fn resumed(&mut self, event_loop: &event_loop::ActiveEventLoop) + { + // Create window + let window = Arc::new( + event_loop + .create_window( + Window::default_attributes() + .with_title("Wgpu Template") + .with_resizable(true), + ) + .unwrap(), + ); + + let state = pollster::block_on(State::new(window.clone())); + self.state = Some(state); + + window.request_redraw(); + window.set_cursor_visible(false); + window + .set_cursor_grab(winit::window::CursorGrabMode::Locked) + .unwrap(); + } + + fn window_event( + &mut self, + event_loop: &event_loop::ActiveEventLoop, + _window_id: winit::window::WindowId, + event: winit::event::WindowEvent, + ) + { + let state = self.state.as_mut().unwrap(); + state.handle_event(&event); + + match event + { + WindowEvent::CloseRequested => + { + event_loop.exit(); + } + WindowEvent::RedrawRequested => + { + state.render(); + state.get_window().request_redraw(); + } + + WindowEvent::Resized(size) => + { + state.resize(size); + } + + WindowEvent::MouseWheel { delta, .. } => + { + state.mouse_wheel(delta); + } + + _ => + {} + } + } + + fn device_event( + &mut self, + _event_loop: &event_loop::ActiveEventLoop, + _device_id: winit::event::DeviceId, + event: winit::event::DeviceEvent, + ) + { + let state = self.state.as_mut().unwrap(); + + #[allow(clippy::single_match)] + match event + { + winit::event::DeviceEvent::MouseMotion { delta } => + { + state.cursor_moved(delta.0 as f32, delta.1 as f32); + } + + _ => + {} + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..9dda782 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,4 @@ +fn main() -> anyhow::Result<()> +{ + cache::run() +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..837cb43 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,432 @@ +use std::collections::HashSet; +use std::f32::consts::PI; +use std::sync::Arc; + +use bytemuck::bytes_of; +use egui_wgpu::ScreenDescriptor; +use gpu_shared::ChunkPushConstants; +use wgpu::ExperimentalFeatures; +use wgpu::Extent3d; +use wgpu::Features; +use wgpu::FeaturesWGPU; +use wgpu::FeaturesWebGPU; +use wgpu::RenderPipeline; +use wgpu::ShaderStages; +use wgpu::TextureDescriptor; +use wgpu::TextureFormat; +use wgpu::TextureUsages; +use wgpu::TextureView; +use wgpu::include_spirv; +use wgpu::include_spirv_raw; +use winit::event::MouseScrollDelta; +use winit::event::WindowEvent; +use winit::keyboard::KeyCode; +use winit::window::Window; + +use crate::egui_renderer::EguiState; + +pub struct State +{ + window: Arc, + device: wgpu::Device, + queue: wgpu::Queue, + size: winit::dpi::PhysicalSize, + surface: wgpu::Surface<'static>, + surface_format: wgpu::TextureFormat, + egui_state: EguiState, + + // Camera + camera: Camera, + + // pressed events + pressed_set: HashSet, + pipeline: RenderPipeline, + depth_buffer: TextureView, +} + +impl State +{ + pub async fn new(window: Arc) -> State + { + let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default()); + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions::default()) + .await + .unwrap(); + let (device, queue) = adapter + .request_device(&wgpu::DeviceDescriptor { + required_features: Features { + features_wgpu: FeaturesWGPU::PUSH_CONSTANTS + | FeaturesWGPU::EXPERIMENTAL_PASSTHROUGH_SHADERS, + features_webgpu: FeaturesWebGPU::empty(), + }, + experimental_features: unsafe { ExperimentalFeatures::enabled() }, + required_limits: wgpu::Limits { + max_push_constant_size: size_of::() as u32, + ..Default::default() + }, + ..Default::default() + }) + .await + .unwrap(); + + let size = window.inner_size(); + + let surface = instance.create_surface(window.clone()).unwrap(); + let cap = surface.get_capabilities(&adapter); + let surface_format = cap.formats[0]; + + // let shaders = include_spirv!(env!("shaders.spv")); + // let shader_module = device.create_shader_module(shaders); + let shaders = include_spirv_raw!(env!("shaders.spv")); + + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("chunk_pipeline"), + layout: Some( + &device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: None, + bind_group_layouts: &[], + push_constant_ranges: &[wgpu::PushConstantRange { + stages: wgpu::ShaderStages::VERTEX, + range: 0..(size_of::() as u32), + }], + }), + ), + vertex: wgpu::VertexState { + module: &shader_module, + entry_point: Some("main_vs"), + buffers: &[], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader_module, + entry_point: Some("main_fs"), + targets: &[Some(wgpu::ColorTargetState { + format: surface_format, + blend: None, + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + ..Default::default() + }, + depth_stencil: Some(wgpu::DepthStencilState { + format: TextureFormat::Depth24PlusStencil8, + depth_write_enabled: true, + depth_compare: wgpu::CompareFunction::Less, + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), + }), + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview: None, + + cache: None, + }); + + let state = State { + egui_state: EguiState::new(&device, surface_format, &window), + + camera: Camera { + eye: glam::Vec3::new(0., 0., 0.), + up: glam::Vec3::ZERO.with_y(1.), + aspect: size.width as f32 / size.height as f32, + fovy: 90., + znear: 0.001, + zfar: 1000., + radius: 2., + yaw: 1., + pitch: 0., + rotation_speed: 0.005, + speed: 0.005, + }, + + window, + queue, + size, + surface, + surface_format, + pipeline, + pressed_set: HashSet::new(), + depth_buffer: device + .create_texture(&TextureDescriptor { + label: Some("Depth buffer"), + size: Extent3d { + width: size.width, + height: size.height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: TextureFormat::Depth24PlusStencil8, + usage: TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }) + .create_view(&Default::default()), + + device, + }; + + // Configure surface for the first time + state.configure_surface(); + + state + } + + pub fn get_window(&self) -> &Window + { + &self.window + } + + pub fn configure_surface(&self) + { + let surface_config = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: self.surface_format, + // Request compatibility with the sRGB-format texture view we‘re going to create later. + view_formats: vec![self.surface_format.add_srgb_suffix()], + alpha_mode: wgpu::CompositeAlphaMode::Auto, + width: self.size.width, + height: self.size.height, + desired_maximum_frame_latency: 2, + present_mode: wgpu::PresentMode::AutoVsync, + }; + self.surface.configure(&self.device, &surface_config); + } + + pub fn resize(&mut self, new_size: winit::dpi::PhysicalSize) + { + self.size = new_size; + + // reconfigure the surface + self.configure_surface(); + + self.depth_buffer = self + .device + .create_texture(&TextureDescriptor { + label: Some("Depth buffer"), + size: Extent3d { + width: new_size.width, + height: new_size.height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: TextureFormat::Depth24PlusStencil8, + usage: TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }) + .create_view(&Default::default()); + } + + pub fn render(&mut self) + { + self.update_camera(); + + // Create texture view + let surface_texture = self + .surface + .get_current_texture() + .expect("failed to acquire next swapchain texture"); + let texture_view = surface_texture + .texture + .create_view(&wgpu::TextureViewDescriptor { + // Without add_srgb_suffix() the image we will be working with + // might not be "gamma correct". + format: Some(self.surface_format.add_srgb_suffix()), + ..Default::default() + }); + + // Renders a GREEN screen + let mut encoder = self.device.create_command_encoder(&Default::default()); + { + let mut renderpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: None, + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &texture_view, + depth_slice: None, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: 0.01, + g: 0.01, + b: 0.01, + a: 1., + }), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &self.depth_buffer, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Clear(1.), + store: wgpu::StoreOp::Discard, + }), + stencil_ops: None, + }), + timestamp_writes: None, + occlusion_query_set: None, + }); + renderpass.set_pipeline(&self.pipeline); + renderpass.set_push_constants( + ShaderStages::VERTEX, + 0, + bytes_of(&ChunkPushConstants { + view_projection: self.camera.view_proj(), + transform: glam::Mat4::IDENTITY, + eye_position: self.camera.eye, + _zero_pad: 0., + }), + ); + renderpass.draw(0..36, 0..1); + + // End the renderpass. + } + // Egui + { + let screen_descriptor = ScreenDescriptor { + size_in_pixels: [self.size.width, self.size.height], + pixels_per_point: 1., + }; + self.egui_state.begin_frame(&self.window); + + egui::Window::new("Hello Window").resizable(true).show( + self.egui_state.context(), + |ui| { + ui.label("Hello, world."); + }, + ); + + self.egui_state.end_frame_and_draw( + &self.device, + &self.queue, + &mut encoder, + &self.window, + &texture_view, + screen_descriptor, + ); + } + + // Submit the command in the queue to execute + self.queue.submit([encoder.finish()]); + self.window.pre_present_notify(); + surface_texture.present(); + } + + // ------------------------------ + // MOVEMENT + // ------------------------------ + pub fn handle_event(&mut self, event: &WindowEvent) + { + self.egui_state.handle_event(&self.window, event); + + if let WindowEvent::KeyboardInput { event, .. } = event + { + match (event.state, event.physical_key) + { + (winit::event::ElementState::Pressed, winit::keyboard::PhysicalKey::Code(c)) => + { + self.pressed_set.insert(c); + } + (winit::event::ElementState::Released, winit::keyboard::PhysicalKey::Code(c)) => + { + self.pressed_set.remove(&c); + } + _ => + {} + } + } + } + + fn update_camera(&mut self) + { + let mut movement = glam::Vec3::new(0., 0., 0.); + if self.pressed_set.contains(&KeyCode::KeyW) + { + movement.z += self.camera.speed; + } + if self.pressed_set.contains(&KeyCode::KeyS) + { + movement.z -= self.camera.speed; + } + + // Left rigth + if self.pressed_set.contains(&KeyCode::KeyA) + { + movement.x += self.camera.speed; + } + if self.pressed_set.contains(&KeyCode::KeyD) + { + movement.x -= self.camera.speed; + } + + let rot_movement = glam::Mat3::from_rotation_y(-self.camera.yaw) + * glam::Mat3::from_rotation_x(-self.camera.pitch) + * movement; + self.camera.eye -= rot_movement; + } + + pub fn cursor_moved(&mut self, x: f32, y: f32) + { + const SENSIBILITY: f32 = 0.0004; + let position = glam::Vec2::new(x, y); + let offset = position * SENSIBILITY; + + self.camera.yaw += offset.x; + self.camera.pitch += offset.y; + } + + pub fn mouse_wheel(&mut self, delta: MouseScrollDelta) + { + if let MouseScrollDelta::LineDelta(_, y) = delta + { + self.camera.speed += y * (self.camera.speed * 0.05); + } + } +} + +#[derive(Clone, Copy)] +pub struct Camera +{ + pub eye: glam::Vec3, + pub up: glam::Vec3, + pub aspect: f32, + pub fovy: f32, + pub znear: f32, + pub zfar: f32, + + pub radius: f32, + + pub yaw: f32, + pub pitch: f32, + pub rotation_speed: f32, + pub speed: f32, +} + +#[rustfmt::skip] +pub const OPENGL_TO_WGPU_MATRIX: glam::Mat4 = glam::Mat4::from_cols( + glam::Vec4::new(1.0, 0.0, 0.0, 0.0), + glam::Vec4::new(0.0, 1.0, 0.0, 0.0), + glam::Vec4::new(0.0, 0.0, 0.5, 0.0), + glam::Vec4::new(0.0, 0.0, 0.5, 1.0), +); + +impl Camera +{ + pub fn view_proj(&self) -> glam::Mat4 + { + let view = glam::Mat4::from_translation(self.eye) + * glam::Mat4::from_rotation_y(-self.yaw) + * glam::Mat4::from_rotation_x(-self.pitch); + let proj = + glam::Mat4::perspective_rh(PI * self.fovy / 180., self.aspect, self.znear, self.zfar); + proj * view.inverse() + } +} diff --git a/src/voxel.rs b/src/voxel.rs new file mode 100644 index 0000000..16cfacd --- /dev/null +++ b/src/voxel.rs @@ -0,0 +1,837 @@ +use std::collections::HashMap; +use std::collections::VecDeque; +use std::hash::Hash; +use std::usize; +use std::vec; + +use itertools::Itertools; + +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct Color(pub f32, pub f32, pub f32, pub f32); + +pub struct NTree +{ + structure: HashMap, NTreeNode>, + depth: u32, +} + +#[derive(Clone, Copy)] +struct NTreeNode +{ + color: Color, + subdivided: bool, +} + +impl NTree +where + [(); N * N * N]:, +{ + pub fn constant(color: Color, depth: u32) -> Self + { + let mut structure = HashMap::new(); + structure.insert( + NTreeLocator::root(), + NTreeNode { + color, + subdivided: false, + }, + ); + Self { structure, depth } + } + + pub fn get(&self, x: usize, y: usize, z: usize) -> Color + { + let mut local_x = x; + let mut local_y = y; + let mut local_z = z; + + let mut size = N.pow(self.depth); + assert!(x < size && y < size && z < size); + + let mut current_loc = NTreeLocator::::root(); + let mut current_node = *self.structure.get(¤t_loc).unwrap(); + + loop + { + if !current_node.subdivided + { + return current_node.color; + } + + size /= N; + + // Descend + let child_x = local_x / size; + let child_y = local_y / size; + let child_z = local_z / size; + + // Node is subdivided, descend + current_loc = current_loc.get_child(child_x, child_y, child_z); + current_node = *self.structure.get(¤t_loc).unwrap(); + + local_x -= child_x * size; + local_y -= child_y * size; + local_z -= child_z * size; + } + } + + pub fn from_arrays_old(chunk: &[Color], depth: u32) -> Self + { + let chunk_width = N.pow(depth); + let mut structure = HashMap::with_capacity(chunk_width * chunk_width * chunk_width); + println!("Inserting voxels"); + for ((x, y), z) in (0..chunk_width) + .cartesian_product(0..chunk_width) + .cartesian_product(0..chunk_width) + { + structure.insert( + NTreeLocator::::from_depth_coords(x, y, z, depth as usize), + NTreeNode:: { + color: chunk[x + y * chunk_width + z * chunk_width * chunk_width], + subdivided: false, + }, + ); + } + + println!("Starting bottom up"); + let mut current_size = 1; + for d in (0..depth).rev() + { + println!("depth: {d}"); + current_size *= N; + for ((x, y), z) in (0..(chunk_width / current_size)) + .cartesian_product(0..(chunk_width / current_size)) + .cartesian_product(0..(chunk_width / current_size)) + { + let loc = NTreeLocator::::from_depth_coords(x, y, z, d as usize); + let children = (0..N) + .cartesian_product(0..N) + .cartesian_product(0..N) + .map(|((cx, cy), cz)| structure.get(&loc.get_child(cx, cy, cz)).unwrap()) + .collect::>(); + + let (can_merge, _) = children.iter().fold((true, None), |acc, child| match acc + { + (_, None) => (true, Some(child.color)), + (b, Some(color)) => + { + (b && !child.subdivided && color == child.color, Some(color)) + } + }); + + let node; + if can_merge + { + node = NTreeNode:: { + color: children[0].color, + subdivided: false, + }; + + //Remove merged childrenn + (0..N) + .cartesian_product(0..N) + .cartesian_product(0..N) + .for_each(|((cx, cy), cz)| { + structure.remove(&loc.get_child(cx, cy, cz)).unwrap(); + }); + } + else + { + node = NTreeNode:: { + color: Color::average( + children + .iter() + .map(|x| x.color) + .collect::>() + .as_slice(), + ), + subdivided: true, + }; + } + structure.insert(loc, node); + } + } + + Self { structure, depth } + } + + pub fn from_arrays(chunk: &[Color], depth: usize) -> Self + { + let width = N.pow(depth as u32); + let mut structure = HashMap::new(); + let mut taken = vec![0; chunk.len()]; // Whether or not the current voxel has been added + + // Voxel insertion/combination pass + for (y, z) in (0..width).cartesian_product(0..width) + { + let mut x = 0; + while x < width + { + if taken[x + y * width + z * width * width] != 0 + { + x += taken[x + y * width + z * width * width]; + continue; + } + + let max_combination_depth = trailing_zeroes::(x) + .unwrap_or(depth) + .min(trailing_zeroes::(y).unwrap_or(depth)) + .min(trailing_zeroes::(z).unwrap_or(depth)); + + let prev_color = chunk[x + y * width + z * width * width]; + let mut locator = NTreeLocator::::from_depth_coords(x, y, z, depth); + //let mut insertion_depth = depth; + let mut block_width = 1; + 'depth_loop: for depth in 1..=max_combination_depth + { + let combination_width = N.pow(depth as u32); + for ((sx, sy), sz) in (0..combination_width) + .cartesian_product(0..combination_width) + .cartesian_product(0..combination_width) + { + let voxel_color = + chunk[(x + sx) + (y + sy) * width + (z + sz) * width * width]; + let voxel_taken = + taken[(x + sx) + (y + sy) * width + (z + sz) * width * width]; + if prev_color != voxel_color || voxel_taken != 0 + { + // Cannot merge further + break 'depth_loop; + } + } + + // At this point, voxel in combination_width^3 block can be merged + block_width = combination_width; + //insertion_depth -= 1; + locator = locator.get_parent(); + } + + structure.insert( + locator, + NTreeNode { + color: prev_color, + subdivided: false, + }, + ); + + // Mark as taken + for ((sx, sy), sz) in (0..block_width) + .cartesian_product(0..block_width) + .cartesian_product(0..block_width) + { + taken[(x + sx) + (y + sy) * width + (z + sz) * width * width] = block_width; + } + + x += block_width; + } + } + + // Increase pass + for d in (0..=(depth - 1)).rev() + { + // Iterate on blocks of depth d + let block_count = N.pow(d as u32); // Number of such blocks along axis + let block_width = width / block_count; // Number of such blocks along axis + for ((bx, by), bz) in (0..(block_count)) + .cartesian_product(0..(block_count)) + .cartesian_product(0..(block_count)) + { + // Get how was the origin voxel merged + let merged_width = taken[(bx * block_width) + + (by * block_width) * width + + (bz * block_width) * width * width]; + + if merged_width >= block_width + { + // Current voxels have been merged in bigger or equal block + continue; + } + + let locator = NTreeLocator::::from_depth_coords(bx, by, bz, d); + + // Otherwise, merge + + // Children CANNOT be merged, Otherwise they would have been already + // Compute average color + + let colors = locator + .iter_children() + .map(|loc| structure.get(&loc).unwrap().color) + .collect::>(); + structure.insert( + locator, + NTreeNode:: { + color: Color::average(&colors), + subdivided: true, + }, + ); + } + } + + Self { + structure, + depth: depth as u32, + } + } + + pub fn set(&mut self, x: usize, y: usize, z: usize, color: Color) + { + let mut local_x = x; + let mut local_y = y; + let mut local_z = z; + + let mut size = N.pow(self.depth); + assert!(x < size && y < size && z < size); + + let mut current_loc = NTreeLocator::::root(); + let mut current_node = *self.structure.get(¤t_loc).unwrap(); + let mut current_depth = 0; + + loop + { + // Subnodes alread with correct color + if current_node.color == color + { + return; + } + + if current_depth == self.depth + { + current_node.color = color; + self.structure.get_mut(¤t_loc).unwrap().color = color; + break; + } + + if !current_node.subdivided + { + // Have to subdivide + let sub_child = NTreeNode:: { + color: current_node.color, + subdivided: false, + }; + // Set node as subdivided + self.structure.get_mut(¤t_loc).unwrap().subdivided = true; + current_node.subdivided = true; + + // Insert new children + current_loc.iter_children().for_each(|x| { + self.structure.insert(x, sub_child); + }); + } + + if current_node.subdivided + { + size /= N; + let child_x = local_x / size; + let child_y = local_y / size; + let child_z = local_z / size; + + // Node is subdivided, descend + current_loc = current_loc.get_child(child_x, child_y, child_z); + current_node = *self.structure.get(¤t_loc).unwrap(); + current_depth += 1; + + local_x -= child_x * size; + local_y -= child_y * size; + local_z -= child_z * size; + } + } + + // Insertion has been done, travel back up to optimise + loop + { + // Try to simplify, compute average + if current_node.subdivided + { + let (compressable, color) = current_loc + .iter_children() + .map(|x| self.structure.get(&x).unwrap()) + .fold((true, None), |state, child| match state + { + (_, None) => (!child.subdivided, Some(child.color)), + (b, Some(c)) => (b && !child.subdivided && c == child.color, Some(c)), + }); + + if compressable + { + // Compress + *self.structure.get_mut(¤t_loc).unwrap() = NTreeNode { + color: color.unwrap(), + subdivided: false, + }; + + // Remove children + current_loc.iter_children().for_each(|x| { + self.structure.remove(&x).unwrap(); + }); + } + else + { + // Update average color + let colors = current_loc + .iter_children() + .map(|x| self.structure.get(&x).unwrap().color) + .collect::>(); + self.structure.get_mut(¤t_loc).unwrap().color = + Color::average(colors.as_slice()); + } + } + + // Travel up + if current_depth == 0 + { + break; // Finished + } + + current_loc = current_loc.get_parent(); + current_node = *self.structure.get(¤t_loc).unwrap(); + current_depth -= 1; + } + } + + pub fn to_gpu_rep(&self) -> (Color, Vec>, Vec<[Color; N * N * N]>) + { + // No root + group by child group + let tile_count = (self.structure.len() - 1) / (N * N * N); + if tile_count == 0 + { + return ( + self.structure + .get(&NTreeLocator::::root()) + .unwrap() + .color, + vec![], + vec![], + ); + } + + let mut structure_tiles = vec![GPUStructureTile::::zero(); tile_count]; + let mut color_tiles = vec![[Color(0., 0., 0., 0.); N * N * N]; tile_count]; + + let mut current_tile = 0usize; + let mut queue = VecDeque::new(); + queue.push_back((NTreeLocator::::root(), current_tile)); + current_tile += 1; + while !queue.is_empty() + { + let (loc, dest_tile) = queue.pop_front().unwrap(); + + let children = loc + .iter_children() + .map(|x| (x, self.structure.get(&x).unwrap())) + .collect::>(); + + let mut structure_tile = GPUStructureTile::::zero(); + let mut color_tile = [Color(0., 0., 0., 0.); N * N * N]; + + for (i, (child_loc, child)) in children.into_iter().enumerate() + { + structure_tile.children[i] = GPUStructureTileIndex::new( + child.subdivided, + child.subdivided, + current_tile as u32, + ); + color_tile[i] = child.color; + + if child.subdivided + { + queue.push_back((child_loc, current_tile)); + current_tile += 1; + } + } + + structure_tiles[dest_tile] = structure_tile; + color_tiles[dest_tile] = color_tile; + } + + println!( + "node count: {}, current_tile: {}", + self.structure.len(), + current_tile + ); + ( + self.structure + .get(&NTreeLocator::::root()) + .unwrap() + .color, + structure_tiles, + color_tiles, + ) + } +} + +#[derive(Clone, Copy)] +pub struct GPUStructureTile +where + [(); N * N * N]:, +{ + children: [GPUStructureTileIndex; N * N * N], +} + +impl GPUStructureTile +where + [(); N * N * N]:, +{ + pub fn zero() -> Self + { + GPUStructureTile { + children: [GPUStructureTileIndex::zero(); N * N * N], + } + } +} + +#[derive(Clone, Copy)] +pub struct GPUStructureTileIndex(u32); + +impl GPUStructureTileIndex +{ + pub fn zero() -> Self + { + GPUStructureTileIndex(0) + } + + pub fn new(subdivided: bool, leaf: bool, ptr: u32) -> Self + { + assert_eq!((ptr >> 30), 0); + GPUStructureTileIndex(ptr | ((leaf as u32) << 30) | ((subdivided as u32) << 31)) + } + + pub fn index(&self) -> u32 + { + self.0 & 0x3FFFFFFFu32 + } + + pub fn subdivided(&self) -> bool + { + (self.0 >> 31) == 1 + } + + pub fn leaf(&self) -> bool + { + ((self.0 >> 30) & 1) == 1 + } +} + +impl Color +{ + pub fn average(colors: &[Color]) -> Color + { + let sum = colors + .iter() + .copied() + .reduce(|Color(r_a, g_a, b_a, a_a), Color(r_b, g_b, b_b, a_b)| { + Color(r_a + r_b, g_a + g_b, b_a + b_b, a_a + a_b) + }) + .unwrap_or(Color(0., 0., 0., 0.)); + //let len = colors.len().max(1) as f32; + let len = colors.iter().map(|x| x.3).sum::(); + + Color(sum.0 / len, sum.1 / len, sum.2 / len, sum.3 / len) + } +} + +#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)] +pub struct NTreeLocator(usize, usize, usize); + +impl NTreeLocator +{ + pub fn root() -> Self + { + Self(1, 1, 1) + } + + pub fn get_child(&self, child_x: usize, child_y: usize, child_z: usize) -> Self + { + assert!(child_x < N && child_y < N && child_z < N); + let mut new_loc_x = self.0; + let mut new_loc_y = self.1; + let mut new_loc_z = self.2; + + // Shift to left three times + new_loc_x *= N; + new_loc_x += child_x; + + new_loc_y *= N; + new_loc_y += child_y; + + new_loc_z *= N; + new_loc_z += child_z; + + Self(new_loc_x, new_loc_y, new_loc_z) + } + + pub fn from_depth_coords(x: usize, y: usize, z: usize, depth: usize) -> Self + { + if depth == 0 + { + return Self::root(); + } + let off = N.pow(depth as u32); + Self(off + x, off + y, off + z) + } + + pub fn iter_children(&self) -> impl Iterator> + { + (0..N) + .cartesian_product(0..N) + .cartesian_product(0..N) + .map(|((x, y), z)| self.get_child(x, y, z)) + } + + pub fn get_parent(&self) -> Self + { + if self.0 == 0 + { + *self + } + else + { + Self(self.0 / N, self.1 / N, self.2 / N) + } + } + + pub fn get_local_location(&self) -> (usize, usize, usize) + { + let mut loc = self.0; + let z = loc % N; + loc /= N; + + let y = loc % N; + loc /= N; + + let x = loc % N; + + (x, y, z) + } + + pub fn is_root(&self) -> bool + { + self.0 == 1 + } +} + +fn trailing_zeroes(mut n: usize) -> Option +{ + if n == 0 + { + return None; + } + let mut i = 0; + while n.is_multiple_of(N) + // n % N == 0 + { + i += 1; + n /= N; + } + Some(i) +} + +#[cfg(test)] +mod test +{ + + use crate::voxel::trailing_zeroes; + use itertools::Itertools; + use rand::Rng; + + use crate::voxel::Color; + use crate::voxel::NTree; + use crate::voxel::NTreeLocator; + + #[test] + pub fn constant() + { + let color = Color(0.5, 0.3, 0.5, 1.); + let depth = 5; + const N: usize = 3; + let width = N.pow(depth); + let ntree = NTree::::constant(color, depth); + + for ((x, y), z) in (0..width) + .cartesian_product(0..width) + .cartesian_product(0..width) + { + assert_eq!(ntree.get(x, y, z), color); + } + } + + #[test] + pub fn full_insert() + { + const DEPTH: u32 = 4; + const N: usize = 3; + const WIDTH: usize = N.pow(DEPTH); + let mut ntree = NTree::::constant(Color(1., 0., 0., 0.), DEPTH); + + let mut rng = rand::rng(); + let mut storage = vec![vec![vec![Color(0., 0., 0., 0.); WIDTH]; WIDTH]; WIDTH]; + for ((x, y), z) in (0..WIDTH) + .cartesian_product(0..WIDTH) + .cartesian_product(0..WIDTH) + { + let color = Color(rng.random(), rng.random(), rng.random(), rng.random()); + storage[x][y][z] = color; + ntree.set(x, y, z, color); + } + + for ((x, y), z) in (0..WIDTH) + .cartesian_product(0..WIDTH) + .cartesian_product(0..WIDTH) + { + let color = ntree.get(x, y, z); + assert_eq!(storage[x][y][z], color); + } + println!("Total nodes {}", ntree.structure.len()); + } + + #[test] + pub fn full_insert_bottom_up() + { + const DEPTH: u32 = 4; + const N: usize = 3; + const WIDTH: usize = N.pow(DEPTH); + + let mut rng = rand::rng(); + let mut storage = vec![Color(0., 0., 0., 0.); WIDTH * WIDTH * WIDTH]; + for ((x, y), z) in (0..WIDTH) + .cartesian_product(0..WIDTH) + .cartesian_product(0..WIDTH) + { + let color = Color(rng.random(), rng.random(), rng.random(), rng.random()); + storage[x + WIDTH * y + WIDTH * WIDTH * z] = color; + } + let ntree = NTree::::from_arrays(&storage, DEPTH as usize); + + for ((x, y), z) in (0..WIDTH) + .cartesian_product(0..WIDTH) + .cartesian_product(0..WIDTH) + { + let color = ntree.get(x, y, z); + assert_eq!(storage[x + WIDTH * y + WIDTH * WIDTH * z], color); + } + println!("Total nodes {}", ntree.structure.len()); + } + + #[test] + pub fn full_insert_const() + { + const DEPTH: u32 = 4; + const N: usize = 3; + const WIDTH: usize = N.pow(DEPTH); + const NEW_COLOR: Color = Color(0., 1., 0., 1.); + let mut ntree = NTree::::constant(Color(1., 0., 1., 0.), DEPTH); + + for ((x, y), z) in (0..WIDTH) + .cartesian_product(0..WIDTH) + .cartesian_product(0..WIDTH) + { + ntree.set(x, y, z, NEW_COLOR); + } + + for ((x, y), z) in (0..WIDTH) + .cartesian_product(0..WIDTH) + .cartesian_product(0..WIDTH) + { + let color = ntree.get(x, y, z); + assert_eq!(NEW_COLOR, color); + } + + println!("Total nodes {}", ntree.structure.len()); + } + + #[test] + pub fn full_insert_quantized() + { + const DEPTH: u32 = 4; + const N: usize = 3; + const WIDTH: usize = N.pow(DEPTH); + let mut ntree = NTree::::constant(Color(0., 0., 0., 0.), DEPTH); + + let mut rng = rand::rng(); + let mut storage = vec![vec![vec![Color(0., 0., 0., 0.); WIDTH]; WIDTH]; WIDTH]; + for ((x, y), z) in (0..WIDTH) + .cartesian_product(0..WIDTH) + .cartesian_product(0..WIDTH) + { + let nbr = rng.random::(); + let mut color = Color(0., 1., 0., 0.); + // Only 1 percent of different color + if nbr > 0.01 + { + color = Color(1., 0., 0., 0.); + } + storage[x][y][z] = color; + ntree.set(x, y, z, color); + } + + for ((x, y), z) in (0..WIDTH) + .cartesian_product(0..WIDTH) + .cartesian_product(0..WIDTH) + { + let color = ntree.get(x, y, z); + assert_eq!(storage[x][y][z], color); + } + println!("Total nodes {}", ntree.structure.len()); + } + + #[test] + pub fn gpu_rep_quantized() + { + const DEPTH: u32 = 4; + const N: usize = 3; + const WIDTH: usize = N.pow(DEPTH); + let mut ntree = NTree::::constant(Color(0., 0., 0., 0.), DEPTH); + + let mut rng = rand::rng(); + let mut storage = vec![vec![vec![Color(0., 0., 0., 0.); WIDTH]; WIDTH]; WIDTH]; + for ((x, y), z) in (0..WIDTH) + .cartesian_product(0..WIDTH) + .cartesian_product(0..WIDTH) + { + let nbr = rng.random::(); + let mut color = Color(0., 1., 0., 0.); + // Only 1 percent of different color + if nbr > 0.01 + { + color = Color(1., 0., 0., 0.); + } + storage[x][y][z] = color; + ntree.set(x, y, z, color); + } + + let (_color, _a, _b) = ntree.to_gpu_rep(); + + drop(_a); + drop(_b); + } + + #[test] + pub fn tree_locator_from_depth_coords() + { + const N: usize = 4; + assert_eq!( + NTreeLocator::::root(), + NTreeLocator::::from_depth_coords(1, 2, 3, 0) + ); + + assert_eq!( + NTreeLocator::::root().get_child(1, 2, 3), + NTreeLocator::::from_depth_coords(1, 2, 3, 1) + ); + + assert_eq!( + NTreeLocator::::root() + .get_child(1, 1, 1) + .get_child(1, 2, 3), + NTreeLocator::::from_depth_coords(4 + 1, 4 + 2, 4 + 3, 2) + ); + } + + #[test] + fn trailing_zeroes_base10() + { + let n = 2139800000; + assert_eq!(trailing_zeroes::<10>(n), Some(5)); + assert_eq!(trailing_zeroes::<10>(0), None); + } +}