diff --git a/Cargo.toml b/Cargo.toml index bcea5c8..6d3c3ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shocovox-rs" -version = "0.2.2" +version = "0.3.0" edition = "2021" authors = ["Dávid Tóth "] license = "MIT OR Apache-2.0" diff --git a/assets/shaders/viewport_render.wgsl b/assets/shaders/viewport_render.wgsl index ba2cdb2..81e29ca 100644 --- a/assets/shaders/viewport_render.wgsl +++ b/assets/shaders/viewport_render.wgsl @@ -671,36 +671,45 @@ struct Viewport { fov: f32, } -@group(2) @binding(0) +@group(0) @binding(1) +var output_texture: texture_storage_2d; + +@group(0) @binding(2) var viewport: Viewport; -@group(2) @binding(1) +@group(0) @binding(3) var octreeMetaData: OctreeMetaData; -@group(2) @binding(2) +@group(0) @binding(4) var nodes: array; -@group(2) @binding(3) +@group(0) @binding(5) var voxels: array; -@fragment -fn fragment(mesh: VertexOutput) -> @location(0) vec4 { - let viewport_ = viewport; //Read only once from global RAM +@compute @workgroup_size(8, 8, 1) +fn update( + @builtin(global_invocation_id) invocation_id: vec3, + @builtin(num_workgroups) num_workgroups: vec3, +) { + let pixel_location = vec2u(invocation_id.xy); + let pixel_location_normalized = vec2f( + f32(invocation_id.x) / f32(num_workgroups.x * 8), + f32(invocation_id.y) / f32(num_workgroups.y * 8) + ); let viewport_up_direction = vec3f(0., 1., 0.); let viewport_right_direction = normalize(cross( - viewport_up_direction, viewport_.direction + viewport_up_direction, viewport.direction )); - let - viewport_bottom_left = viewport_.origin - + (viewport_.direction * viewport_.fov) - - (viewport_right_direction * (viewport_.size.x / 2.)) - - (viewport_up_direction * (viewport_.size.y / 2.)) + let viewport_bottom_left = viewport.origin + + (viewport.direction * viewport.fov) + - (viewport_right_direction * (viewport.size.x / 2.)) + - (viewport_up_direction * (viewport.size.y / 2.)) ; let ray_endpoint = viewport_bottom_left - + viewport_right_direction * viewport_.size.x * mesh.uv.x - + viewport_up_direction * viewport_.size.y * (1. - mesh.uv.y) + + viewport_right_direction * viewport.size.x * f32(pixel_location_normalized.x) + + viewport_up_direction * viewport.size.y * (1. - f32(pixel_location_normalized.y)) ; - var ray = Line(ray_endpoint, normalize(ray_endpoint - viewport_.origin)); + var ray = Line(ray_endpoint, normalize(ray_endpoint - viewport.origin)); var ray_result = get_by_ray(ray); var rgb_result = vec3f(0.5,0.5,0.5); @@ -711,7 +720,8 @@ fn fragment(mesh: VertexOutput) -> @location(0) vec4 { let result_with_lights = ray_result.albedo.rgb * diffuse_light_strength; rgb_result = result_with_lights.rgb; } - return vec4(rgb_result, 1.0); + + textureStore(output_texture, pixel_location, vec4f(rgb_result, 1.)); } // Note: should be const diff --git a/examples/bevy_wgpu_render.rs b/examples/bevy_wgpu_render.rs index 6c078f3..76bffed 100644 --- a/examples/bevy_wgpu_render.rs +++ b/examples/bevy_wgpu_render.rs @@ -1,14 +1,27 @@ #[cfg(feature = "bevy_wgpu")] -use bevy::prelude::*; +use bevy::{prelude::*, window::WindowPlugin}; + +#[cfg(feature = "bevy_wgpu")] +use shocovox_rs::octree::{ + raytracing::{ShocoVoxRenderPlugin, ShocoVoxViewingGlass, Viewport}, + V3c, +}; + +#[cfg(feature = "bevy_wgpu")] +const DISPLAY_RESOLUTION: [u32; 2] = [1024, 768]; + #[cfg(feature = "bevy_wgpu")] -use shocovox_rs::{octree::raytracing::OctreeViewMaterial, octree::V3c}; +const ARRAY_DIMENSION: u32 = 64; #[cfg(feature = "bevy_wgpu")] fn main() { App::new() + .insert_resource(ClearColor(Color::BLACK)) .add_plugins(( - DefaultPlugins, - MaterialPlugin::::default(), + DefaultPlugins.set(WindowPlugin::default()), + ShocoVoxRenderPlugin { + resolution: DISPLAY_RESOLUTION, + }, )) .add_systems(Startup, setup) .add_systems(Update, rotate_camera) @@ -17,38 +30,12 @@ fn main() { } #[cfg(feature = "bevy_wgpu")] -const ARRAY_DIMENSION: u32 = 64; - -#[cfg(feature = "bevy_wgpu")] -fn setup( - mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, -) { - use shocovox_rs::octree::{raytracing::Viewport, types::Albedo}; - - commands.spawn(PointLightBundle { - point_light: PointLight { - intensity: 3000.0, - ..Default::default() - }, - transform: Transform::from_xyz(-3.0, 2.0, -1.0), - ..Default::default() - }); - commands.spawn(PointLightBundle { - point_light: PointLight { - intensity: 3000.0, - ..Default::default() - }, - transform: Transform::from_xyz(3.0, 2.0, 1.0), - ..Default::default() - }); - - commands.spawn(Camera3dBundle { - transform: Transform::from_xyz(5.0, 5.0, 7.0).looking_at(Vec3::new(4., 1., 0.0), Vec3::Y), - ..Default::default() - }); - +fn setup(mut commands: Commands, images: ResMut>) { + let origin = Vec3::new( + ARRAY_DIMENSION as f32 * 2., + ARRAY_DIMENSION as f32 / 2., + ARRAY_DIMENSION as f32 * -2., + ); commands.spawn(DomePosition { yaw: 0. }); // fill octree with data @@ -95,27 +82,26 @@ fn setup( } } } - let quad_size = 10.; - let mesh_handle = meshes.add(Mesh::from(Rectangle { - half_size: Vec2::new(quad_size, quad_size) / 2., - })); - let origin = Vec3::new( - ARRAY_DIMENSION as f32 * 2., - ARRAY_DIMENSION as f32 / 2., - ARRAY_DIMENSION as f32 * -2., + let viewing_glass = tree.create_bevy_view( + &Viewport { + direction: (Vec3::new(0., 0., 0.) - origin).normalize(), + origin, + size: Vec2::new(10., 10.), + fov: 3., + }, + DISPLAY_RESOLUTION, + images, ); - let material_handle = materials.add(tree.create_bevy_material_view(&Viewport { - direction: (Vec3::new(0., 0., 0.) - origin).normalize(), - origin, - size: Vec2::new(10., 10.), - fov: 3., - })); - commands.spawn(MaterialMeshBundle { - mesh: mesh_handle.clone(), - material: material_handle.clone(), - transform: Transform::from_xyz(quad_size + 0.5, 0.0, 0.0), - ..Default::default() + commands.spawn(SpriteBundle { + sprite: Sprite { + custom_size: Some(Vec2::new(1024., 768.)), + ..default() + }, + texture: viewing_glass.output_texture.clone(), + ..default() }); + commands.spawn(Camera2dBundle::default()); + commands.insert_resource(viewing_glass); } #[cfg(feature = "bevy_wgpu")] @@ -127,10 +113,10 @@ struct DomePosition { #[cfg(feature = "bevy_wgpu")] fn rotate_camera( mut angles_query: Query<&mut DomePosition>, - mut mats: ResMut>, + mut viewing_glass: ResMut, ) { let angle = { - let addition = ARRAY_DIMENSION as f32 / 1024.; + let addition = ARRAY_DIMENSION as f32 / 10.; let angle = angles_query.single().yaw + addition; if angle < 360. { angle @@ -140,31 +126,27 @@ fn rotate_camera( }; angles_query.single_mut().yaw = angle; - for (_mat_handle, mat) in mats.as_mut().iter_mut() { - let radius = ARRAY_DIMENSION as f32 * 1.3; - mat.viewport.origin = Vec3::new( - ARRAY_DIMENSION as f32 / 2. + angle.sin() * radius, - ARRAY_DIMENSION as f32 / 2., - ARRAY_DIMENSION as f32 / 2. + angle.cos() * radius, - ); - mat.viewport.direction = (Vec3::new( - ARRAY_DIMENSION as f32 / 2., - ARRAY_DIMENSION as f32 / 2., - ARRAY_DIMENSION as f32 / 2., - ) - mat.viewport.origin) - .normalize(); - } + let radius = ARRAY_DIMENSION as f32 * 1.3; + viewing_glass.viewport.origin = Vec3::new( + ARRAY_DIMENSION as f32 / 2. + angle.sin() * radius, + ARRAY_DIMENSION as f32 / 2., + ARRAY_DIMENSION as f32 / 2. + angle.cos() * radius, + ); + viewing_glass.viewport.direction = (Vec3::new( + ARRAY_DIMENSION as f32 / 2., + ARRAY_DIMENSION as f32 / 2., + ARRAY_DIMENSION as f32 / 2., + ) - viewing_glass.viewport.origin) + .normalize(); } #[cfg(feature = "bevy_wgpu")] -fn handle_zoom(keys: Res>, mut mats: ResMut>) { - for (_mat_handle, mat) in mats.as_mut().iter_mut() { - if keys.pressed(KeyCode::ArrowUp) { - mat.viewport.size *= 1.1; - } - if keys.pressed(KeyCode::ArrowDown) { - mat.viewport.size *= 0.9; - } +fn handle_zoom(keys: Res>, mut viewing_glass: ResMut) { + if keys.pressed(KeyCode::ArrowUp) { + viewing_glass.viewport.size *= 1.1; + } + if keys.pressed(KeyCode::ArrowDown) { + viewing_glass.viewport.size *= 0.9; } } diff --git a/src/octree/raytracing/classic_raytracing_on_bevy_wgpu.rs b/src/octree/raytracing/bevy/data.rs similarity index 88% rename from src/octree/raytracing/classic_raytracing_on_bevy_wgpu.rs rename to src/octree/raytracing/bevy/data.rs index 06f925f..6782cc5 100644 --- a/src/octree/raytracing/classic_raytracing_on_bevy_wgpu.rs +++ b/src/octree/raytracing/bevy/data.rs @@ -1,22 +1,18 @@ use crate::object_pool::empty_marker; + use crate::octree::{ - raytracing::types::{OctreeMetaData, OctreeViewMaterial, SizedNode, Viewport, Voxelement}, + raytracing::{ + bevy::create_ouput_texture, + bevy::types::{OctreeMetaData, ShocoVoxViewingGlass, SizedNode, Viewport, Voxelement}, + }, types::{NodeChildrenArray, NodeContent}, + Octree, VoxelData, }; use bevy::{ - math::Vec3, - pbr::Material, - render::{color::Color, render_resource::ShaderRef}, + asset::Assets, ecs::system::ResMut, math::Vec3, render::color::Color, render::texture::Image, }; -impl Material for OctreeViewMaterial { - fn fragment_shader() -> ShaderRef { - "shaders/viewport_render.wgsl".into() - } -} - -use crate::octree::{Octree, VoxelData}; impl Octree where T: Default + Clone + VoxelData, @@ -58,7 +54,12 @@ where meta } - pub fn create_bevy_material_view(&self, viewport: &Viewport) -> OctreeViewMaterial { + pub fn create_bevy_view( + &self, + viewport: &Viewport, + resolution: [u32; 2], + images: ResMut>, + ) -> ShocoVoxViewingGlass { let meta = OctreeMetaData { octree_size: self.octree_size, voxel_matrix_dim: DIM as u32, @@ -113,7 +114,9 @@ where } nodes.push(sized_node); } - OctreeViewMaterial { + + ShocoVoxViewingGlass { + output_texture: create_ouput_texture(resolution, images), viewport: *viewport, meta, nodes, diff --git a/src/octree/raytracing/bevy/mod.rs b/src/octree/raytracing/bevy/mod.rs new file mode 100644 index 0000000..4ee321c --- /dev/null +++ b/src/octree/raytracing/bevy/mod.rs @@ -0,0 +1,167 @@ +mod data; +pub mod types; + +pub use crate::octree::raytracing::bevy::types::{ + ShocoVoxRenderPlugin, ShocoVoxViewingGlass, Viewport, +}; + +use crate::octree::raytracing::bevy::types::{ + ShocoVoxLabel, ShocoVoxRenderNode, ShocoVoxRenderPipeline, +}; + +use bevy::{ + app::{App, Plugin}, + asset::{AssetServer, Assets, Handle}, + ecs::system::{Res, ResMut}, + ecs::world::{FromWorld, World}, + prelude::IntoSystemConfigs, + render::{ + extract_resource::ExtractResourcePlugin, + render_asset::{RenderAssetUsages, RenderAssets}, + render_graph, + render_graph::RenderGraph, + render_resource::{ + AsBindGroup, CachedPipelineState, ComputePassDescriptor, ComputePipelineDescriptor, + Extent3d, PipelineCache, TextureDimension, TextureFormat, TextureUsages, + }, + renderer::{RenderContext, RenderDevice}, + texture::{FallbackImage, Image}, + Render, RenderApp, RenderSet, + }, +}; + +use std::borrow::Cow; + +impl FromWorld for ShocoVoxRenderPipeline { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + let viewing_glass_bind_group_layout = + ShocoVoxViewingGlass::bind_group_layout(render_device); + let shader = world + .resource::() + .load("shaders/viewport_render.wgsl"); + let pipeline_cache = world.resource::(); + let update_pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: None, + layout: vec![viewing_glass_bind_group_layout.clone()], + push_constant_ranges: Vec::new(), + shader, + shader_defs: vec![], + entry_point: Cow::from("update"), + }); + + ShocoVoxRenderPipeline { + viewing_glass_bind_group_layout, + update_pipeline, + bind_group: None, + } + } +} + +fn prepare_bind_group( + gpu_images: Res>, + fallback_image: Res, + render_device: Res, + mut pipeline: ResMut, + octree_viewing_glass: Res, +) { + let bind_group = octree_viewing_glass + .as_bind_group( + &pipeline.viewing_glass_bind_group_layout, + &render_device, + &gpu_images, + &fallback_image, + ) + .ok() + .unwrap(); + pipeline.bind_group = Some(bind_group.bind_group); +} + +pub(crate) fn create_ouput_texture( + resolution: [u32; 2], + mut images: ResMut>, +) -> Handle { + let mut output_texture = Image::new_fill( + Extent3d { + width: resolution[0], + height: resolution[1], + depth_or_array_layers: 1, + }, + TextureDimension::D2, + &[0, 0, 0, 255], + TextureFormat::Rgba8Unorm, + RenderAssetUsages::RENDER_WORLD, + ); + output_texture.texture_descriptor.usage = + TextureUsages::COPY_DST | TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING; + images.add(output_texture) +} + +impl Plugin for ShocoVoxRenderPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(ExtractResourcePlugin::::default()); + let render_app = app.sub_app_mut(RenderApp); + render_app.add_systems( + Render, + prepare_bind_group.in_set(RenderSet::PrepareBindGroups), + ); + + let mut render_graph = render_app.world.resource_mut::(); + render_graph.add_node( + ShocoVoxLabel, + ShocoVoxRenderNode { + ready: false, + resolution: self.resolution, + }, + ); + render_graph.add_node_edge(ShocoVoxLabel, bevy::render::graph::CameraDriverLabel); + } + + fn finish(&self, app: &mut App) { + let render_app = app.sub_app_mut(RenderApp); + render_app.init_resource::(); + } +} + +const WORKGROUP_SIZE: u32 = 8; +impl render_graph::Node for ShocoVoxRenderNode { + fn update(&mut self, world: &mut World) { + let pipeline = world.resource::(); + let pipeline_cache = world.resource::(); + if !self.ready { + if let CachedPipelineState::Ok(_) = + pipeline_cache.get_compute_pipeline_state(pipeline.update_pipeline) + { + self.ready = true; + } + } + } + + fn run( + &self, + _graph: &mut render_graph::RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), render_graph::NodeRunError> { + let pipeline_cache = world.resource::(); + let pipeline = world.resource::(); + + if self.ready { + let mut pass = render_context + .command_encoder() + .begin_compute_pass(&ComputePassDescriptor::default()); + + pass.set_bind_group(0, pipeline.bind_group.as_ref().unwrap(), &[]); + let pipeline = pipeline_cache + .get_compute_pipeline(pipeline.update_pipeline) + .unwrap(); + pass.set_pipeline(pipeline); + pass.dispatch_workgroups( + self.resolution[0] / WORKGROUP_SIZE, + self.resolution[1] / WORKGROUP_SIZE, + 1, + ); + } + Ok(()) + } +} diff --git a/src/octree/raytracing/bevy/types.rs b/src/octree/raytracing/bevy/types.rs new file mode 100644 index 0000000..4619cdc --- /dev/null +++ b/src/octree/raytracing/bevy/types.rs @@ -0,0 +1,109 @@ +use bevy::{ + asset::Handle, + ecs::component::Component, + ecs::system::Resource, + math::{Vec2, Vec3}, + reflect::TypePath, + render::{ + color::Color, + extract_resource::ExtractResource, + render_graph::RenderLabel, + render_resource::{ + AsBindGroup, BindGroup, BindGroupLayout, CachedComputePipelineId, ShaderType, + }, + texture::Image, + }, +}; + +#[derive(Clone, ShaderType)] +pub(crate) struct Voxelement { + pub(crate) albedo: Color, + pub(crate) content: u32, +} + +#[derive(Clone, ShaderType)] +pub(crate) struct SizedNode { + /// Composite field: + /// - Byte 1: Boolean value, true in case node is a leaf + /// - In case of internal nodes: + /// - Byte 2: TBD + /// - Byte 3: TBD + /// - Byte 4: Lvl2 Occupancy bitmap + /// - In case of leaf nodes: + /// - Byte 2: TBD + /// - Byte 3: TBD + /// - Byte 4: TBD + pub(crate) sized_node_meta: u32, + + /// - In case of internal nodes: + /// - Index values of node children + /// - In case of leaf nodes: + /// - Byte 1-4: Occupancy bitmap MSB + /// - Byte 5-8: Occupancy bitmap LSB + /// - Byte 9-12: TBD + /// - Byte 13-16: TBD + /// - Byte 17-20: TBD + /// - Byte 21-24: TBD + /// - Byte 25-28: TBD + /// - Byte 29-32: TBD + pub(crate) children: [u32; 8], + + /// index of where the voxel values contained in the node start inside the voxels buffer, + /// or a "none_value". Should the field contain an index, the next voxel_matrix_dim^3 elements + /// inside the @voxels array count as part of the voxels associated with the node + pub(crate) voxels_start_at: u32, +} + +#[derive(Clone, ShaderType)] +pub struct OctreeMetaData { + pub(crate) octree_size: u32, + pub(crate) voxel_matrix_dim: u32, + pub ambient_light_color: Color, + pub ambient_light_position: Vec3, +} + +#[derive(Clone, Copy, ShaderType)] +pub struct Viewport { + pub origin: Vec3, + pub direction: Vec3, + pub size: Vec2, + pub fov: f32, +} + +pub struct ShocoVoxRenderPlugin { + pub resolution: [u32; 2], +} + +#[derive(Resource, Clone, AsBindGroup, TypePath, ExtractResource)] +#[type_path = "shocovox::gpu::ShocoVoxViewingGlass"] +pub struct ShocoVoxViewingGlass { + #[storage_texture(1, image_format = Rgba8Unorm, access = ReadWrite)] + pub output_texture: Handle, + + #[uniform(2, visibility(compute))] + pub viewport: Viewport, + + #[uniform(3, visibility(compute))] + pub(crate) meta: OctreeMetaData, + + #[storage(4, visibility(compute))] + pub(crate) nodes: Vec, + + #[storage(5, visibility(compute))] + pub(crate) voxels: Vec, +} + +#[derive(Resource)] +pub(crate) struct ShocoVoxRenderPipeline { + pub(crate) viewing_glass_bind_group_layout: BindGroupLayout, + pub(crate) update_pipeline: CachedComputePipelineId, + pub(crate) bind_group: Option, +} + +#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)] +pub(crate) struct ShocoVoxLabel; + +pub(crate) struct ShocoVoxRenderNode { + pub(crate) ready: bool, + pub(crate) resolution: [u32; 2], +} diff --git a/src/octree/raytracing/mod.rs b/src/octree/raytracing/mod.rs index 611aedd..95bb7bf 100644 --- a/src/octree/raytracing/mod.rs +++ b/src/octree/raytracing/mod.rs @@ -3,9 +3,9 @@ mod tests; mod types; #[cfg(feature = "bevy_wgpu")] -pub mod classic_raytracing_on_bevy_wgpu; +pub mod bevy; pub use crate::spatial::raytracing::Ray; #[cfg(feature = "bevy_wgpu")] -pub use types::{OctreeViewMaterial, Viewport}; +pub use bevy::types::{ShocoVoxRenderPlugin, ShocoVoxViewingGlass, Viewport}; diff --git a/src/octree/raytracing/types.rs b/src/octree/raytracing/types.rs index bd7db4b..c5a38e8 100644 --- a/src/octree/raytracing/types.rs +++ b/src/octree/raytracing/types.rs @@ -1,16 +1,6 @@ use crate::octree::{Cube, V3c}; use crate::spatial::raytracing::CubeRayIntersection; -#[cfg(feature = "bevy_wgpu")] -use bevy::{ - asset::Asset, - ecs::system::Resource, - math::{Vec2, Vec3}, - reflect::TypePath, - render::render_resource::AsBindGroup, - render::{color::Color, render_resource::ShaderType}, -}; - pub(crate) struct NodeStackItem { pub(crate) bounds_intersection: CubeRayIntersection, pub(crate) bounds: Cube, @@ -19,79 +9,3 @@ pub(crate) struct NodeStackItem { pub(crate) target_octant: u8, pub(crate) child_center: V3c, } - -#[cfg(feature = "bevy_wgpu")] -#[derive(Clone, ShaderType)] -pub(crate) struct Voxelement { - pub(crate) albedo: Color, - pub(crate) content: u32, -} - -#[cfg(feature = "bevy_wgpu")] -#[derive(Clone, ShaderType)] -pub(crate) struct SizedNode { - /// Composite field: - /// - Byte 1: Boolean value, true in case node is a leaf - /// - In case of internal nodes: - /// - Byte 2: TBD - /// - Byte 3: TBD - /// - Byte 4: Lvl2 Occupancy bitmap - /// - In case of leaf nodes: - /// - Byte 2: TBD - /// - Byte 3: TBD - /// - Byte 4: TBD - pub(crate) sized_node_meta: u32, - - /// - In case of internal nodes: - /// - Index values of node children - /// - In case of leaf nodes: - /// - Byte 1-4: Occupancy bitmap MSB - /// - Byte 5-8: Occupancy bitmap LSB - /// - Byte 9-12: TBD - /// - Byte 13-16: TBD - /// - Byte 17-20: TBD - /// - Byte 21-24: TBD - /// - Byte 25-28: TBD - /// - Byte 29-32: TBD - pub(crate) children: [u32; 8], - - /// index of where the voxel values contained in the node start inside the voxels buffer, - /// or a "none_value". Should the field contain an index, the next voxel_matrix_dim^3 elements - /// inside the @voxels array count as part of the voxels associated with the node - pub(crate) voxels_start_at: u32, -} - -#[cfg(feature = "bevy_wgpu")] -#[derive(Clone, ShaderType)] -pub struct OctreeMetaData { - pub(crate) octree_size: u32, - pub(crate) voxel_matrix_dim: u32, - pub ambient_light_color: Color, - pub ambient_light_position: Vec3, -} - -#[cfg(feature = "bevy_wgpu")] -#[derive(Clone, Copy, ShaderType)] -pub struct Viewport { - pub origin: Vec3, - pub direction: Vec3, - pub size: Vec2, - pub fov: f32, -} - -#[cfg(feature = "bevy_wgpu")] -#[derive(Asset, Resource, Clone, AsBindGroup, TypePath)] -#[type_path = "shocovox::gpu::OctreeViewMaterial"] -pub struct OctreeViewMaterial { - #[uniform(0)] - pub viewport: Viewport, - - #[uniform(1)] - pub(crate) meta: OctreeMetaData, - - #[storage(2)] - pub(crate) nodes: Vec, - - #[storage(3)] - pub(crate) voxels: Vec, -}