A whole lot of garbage
This commit is contained in:
+6539
File diff suppressed because it is too large
Load Diff
Executable
+6
@@ -0,0 +1,6 @@
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["app","rendering", "terrain"]
|
||||
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 3
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
/target
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "voxtech"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
# voxtech_terrain = { path = "../terrain" }
|
||||
# voxtech_rendering = { path = "../rendering" }
|
||||
|
||||
tracing-subscriber = "0.3.23"
|
||||
tracing = { version = "0.1.44", features = ["log"] }
|
||||
|
||||
bevy = { version = "0.19.0-rc.2", features = ["dynamic_linking"] }
|
||||
Executable
+150
@@ -0,0 +1,150 @@
|
||||
use std::range::Range;
|
||||
|
||||
use bevy::{
|
||||
DefaultPlugins,
|
||||
asset::io::embedded::GetAssetServer,
|
||||
core_pipeline::core_3d::*,
|
||||
ecs::{component::Component, query::*, system::SystemParamItem},
|
||||
mesh::Mesh3d,
|
||||
pbr::{SetMeshBindGroup, SetMeshViewBindGroup, SetMeshViewEmptyBindGroup},
|
||||
prelude::*,
|
||||
render::{
|
||||
RenderSystems::Queue,
|
||||
render_phase::*,
|
||||
render_resource::*,
|
||||
settings::{Backends, RenderCreation, WgpuSettings},
|
||||
*,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Component, Clone, Copy)]
|
||||
struct CustomDrawComponent(Range<usize>);
|
||||
|
||||
type CustomDrawSequence = (
|
||||
SetItemPipeline,
|
||||
SetMeshViewBindGroup<0>,
|
||||
SetMeshViewEmptyBindGroup<1>,
|
||||
SetMeshBindGroup<2>,
|
||||
CustomDrawCommand,
|
||||
);
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct CustomPipeline {
|
||||
pub pipeline_id: CachedRenderPipelineId,
|
||||
}
|
||||
|
||||
impl FromWorld for CustomPipeline {
|
||||
fn from_world(world: &mut World) -> Self {
|
||||
// 1. Get the pipeline cache
|
||||
let pipeline_cache = world.resource::<PipelineCache>();
|
||||
let shader = world.get_asset_server().load("shaders/shader.wgsl");
|
||||
|
||||
// 2. Define your pipeline descriptor (Shaders, vertex buffers, etc.)
|
||||
let descriptor = RenderPipelineDescriptor {
|
||||
label: Some("custom_draw_pipeline".into()),
|
||||
layout: vec![], // Define your bind group layouts here if needed
|
||||
vertex: VertexState {
|
||||
shader: shader.clone(),
|
||||
shader_defs: vec![],
|
||||
entry_point: Some("vertex".into()),
|
||||
buffers: vec![],
|
||||
},
|
||||
fragment: Some(FragmentState {
|
||||
shader,
|
||||
shader_defs: vec![],
|
||||
entry_point: Some("fragment".into()),
|
||||
targets: vec![Some(ColorTargetState {
|
||||
format: TextureFormat::Bgra8UnormSrgb,
|
||||
blend: Some(BlendState::REPLACE),
|
||||
write_mask: ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
primitive: PrimitiveState::default(),
|
||||
depth_stencil: None,
|
||||
multisample: MultisampleState::default(),
|
||||
zero_initialize_workgroup_memory: false,
|
||||
immediate_size: 0,
|
||||
};
|
||||
|
||||
// 3. Queue the pipeline for compilation and store the ID
|
||||
let pipeline_id = pipeline_cache.queue_render_pipeline(descriptor);
|
||||
|
||||
CustomPipeline { pipeline_id }
|
||||
}
|
||||
}
|
||||
|
||||
struct CustomDrawCommand;
|
||||
|
||||
impl<P: PhaseItem> RenderCommand<P> for CustomDrawCommand {
|
||||
type Param = ();
|
||||
|
||||
type ViewQuery = ();
|
||||
|
||||
type ItemQuery = &'static CustomDrawComponent;
|
||||
|
||||
fn render<'w>(
|
||||
_item: &P,
|
||||
_view: ROQueryItem<'w, '_, Self::ViewQuery>,
|
||||
entity: Option<ROQueryItem<'w, '_, Self::ItemQuery>>,
|
||||
param: SystemParamItem<'w, '_, Self::Param>,
|
||||
pass: &mut TrackedRenderPass<'w>,
|
||||
) -> RenderCommandResult {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
fn enqueue_draws(
|
||||
mut views: Query<&mut RenderPhase<Opaque3d>>,
|
||||
polys: Query<Entity, With<MyPolyBatch>>,
|
||||
draw_functions: Res<DrawFunctions<Opaque3d>>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn main() -> AppExit {
|
||||
let mut app = App::default();
|
||||
app.add_plugins(DefaultPlugins.set(RenderPlugin {
|
||||
render_creation: RenderCreation::Automatic(Box::new(WgpuSettings {
|
||||
backends: Some(Backends::VULKAN | Backends::GL),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
}));
|
||||
app.add_systems(Startup, setup);
|
||||
|
||||
if let Some(app) = app.get_sub_app_mut(RenderApp) {
|
||||
app.add_render_command::<Opaque3d, CustomDrawCommand>();
|
||||
app.add_systems(Render, enqueue_draws.in_set(Queue));
|
||||
}
|
||||
|
||||
app.run()
|
||||
}
|
||||
|
||||
/// set up a simple 3D scene
|
||||
fn setup(
|
||||
mut commands: Commands,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
) {
|
||||
// circular base
|
||||
commands.spawn((
|
||||
Mesh3d(meshes.add(Circle::new(4.0))),
|
||||
MeshMaterial3d(materials.add(Color::WHITE)),
|
||||
Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
|
||||
));
|
||||
// cube
|
||||
commands.spawn((
|
||||
Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
|
||||
MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))),
|
||||
Transform::from_xyz(0.0, 0.5, 0.0),
|
||||
));
|
||||
// light
|
||||
commands.spawn((
|
||||
PointLight { ..default() },
|
||||
Transform::from_xyz(4.0, 8.0, 4.0),
|
||||
));
|
||||
// camera
|
||||
commands.spawn((
|
||||
Camera3d::default(),
|
||||
Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
|
||||
));
|
||||
}
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
/target
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "voxtech_rendering"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
voxtech_terrain = { path = "../terrain" }
|
||||
|
||||
bytemuck = "1.25.0"
|
||||
glam = "0.33.0"
|
||||
modular-bitfield = "0.13.1"
|
||||
pollster = "0.4.0"
|
||||
tracing = "0.1.44"
|
||||
wgpu = { version = "29.0.3", default-features = false, features = ["vulkan", "std", "wgsl", "webgpu"] }
|
||||
winit = "0.30.13"
|
||||
|
||||
bevy_ecs = "0.18.1"
|
||||
bevy_app = "0.18.1"
|
||||
rayon = "1.12.0"
|
||||
crossbeam = "0.8.4"
|
||||
rangemap = "1.7.1"
|
||||
Executable
+220
@@ -0,0 +1,220 @@
|
||||
use bevy_app::{App, Plugin, PostUpdate};
|
||||
use bevy_ecs::{
|
||||
component::Component,
|
||||
entity::Entity,
|
||||
query::{With, Without},
|
||||
resource::Resource,
|
||||
schedule::IntoScheduleConfigs,
|
||||
system::{Commands, Query, Res, ResMut},
|
||||
};
|
||||
use crossbeam::channel::{Receiver, Sender};
|
||||
use std::{ops::Range, sync::Arc, thread::JoinHandle, time::Instant};
|
||||
use tracing::debug;
|
||||
use voxtech_terrain::chunk::{ChunkData, SIZE, UnsafeChunkData};
|
||||
|
||||
use crate::{
|
||||
chunk::renderer::{ChunkRenderer, Face, FaceColour, FaceDirection},
|
||||
plugin::RenderPluginState,
|
||||
};
|
||||
|
||||
#[derive(Debug, Component)]
|
||||
pub(crate) struct ChunkMesh {
|
||||
range: Range<usize>,
|
||||
last_update: Instant,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Component)]
|
||||
pub(crate) struct ChunkMeshDirty;
|
||||
|
||||
pub(crate) struct ChunkMesher {
|
||||
pub thread_count: usize,
|
||||
}
|
||||
|
||||
impl Plugin for ChunkMesher {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.insert_resource(ChunkMesherState::new(self.thread_count));
|
||||
app.add_systems(PostUpdate, tag_not_meshed);
|
||||
app.add_systems(PostUpdate, schedule_dirty.after(tag_not_meshed));
|
||||
app.add_systems(PostUpdate, submit_meshed.after(schedule_dirty));
|
||||
}
|
||||
|
||||
fn cleanup(&self, app: &mut App) {
|
||||
app.world_mut().remove_resource::<ChunkMesherState>();
|
||||
}
|
||||
}
|
||||
|
||||
struct MeshRequest {
|
||||
time: Instant,
|
||||
entity: Entity,
|
||||
voxels: Box<UnsafeChunkData>,
|
||||
}
|
||||
|
||||
struct MeshResult {
|
||||
time: Instant,
|
||||
entity: Entity,
|
||||
faces: Vec<Face>,
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
struct ChunkMesherState {
|
||||
sender: Sender<MeshRequest>,
|
||||
receiver: Receiver<MeshResult>,
|
||||
threads: Vec<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl ChunkMesherState {
|
||||
pub fn new(thread_count: usize) -> Self {
|
||||
let (request_sender, request_receiver) = crossbeam::channel::unbounded();
|
||||
let request_receiver = Arc::new(request_receiver);
|
||||
|
||||
let (result_sender, result_receiver) = crossbeam::channel::unbounded();
|
||||
let result_sender = Arc::new(result_sender);
|
||||
|
||||
let mut threads = Vec::with_capacity(thread_count);
|
||||
|
||||
for _ in 0..thread_count {
|
||||
let sender = result_sender.clone();
|
||||
let receiver = request_receiver.clone();
|
||||
threads.push(std::thread::spawn(move || {
|
||||
loop {
|
||||
let Ok(MeshRequest {
|
||||
time,
|
||||
entity,
|
||||
voxels,
|
||||
}) = receiver.recv()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
debug!("Meshing {entity:?}...");
|
||||
let now = Instant::now();
|
||||
let mut faces = vec![];
|
||||
generate_chunk_mesh::<{ FaceDirection::Front as usize }>(&voxels, &mut faces);
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
debug!("Meshed {entity:?} in {:?} ({} faces)", elapsed, faces.len());
|
||||
|
||||
if !sender
|
||||
.send(MeshResult {
|
||||
time,
|
||||
entity,
|
||||
faces,
|
||||
})
|
||||
.is_ok()
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
Self {
|
||||
sender: request_sender,
|
||||
receiver: result_receiver,
|
||||
threads,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn tag_not_meshed(
|
||||
mut commands: Commands,
|
||||
query: Query<Entity, (With<ChunkData>, Without<ChunkMesh>)>,
|
||||
) {
|
||||
for entity in query.iter() {
|
||||
commands.entity(entity).insert((
|
||||
ChunkMeshDirty,
|
||||
ChunkMesh {
|
||||
range: 0..0,
|
||||
last_update: Instant::now(),
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn schedule_dirty(
|
||||
mut commands: Commands,
|
||||
state: Res<ChunkMesherState>,
|
||||
query: Query<(Entity, &mut ChunkData), With<ChunkMeshDirty>>,
|
||||
) {
|
||||
for (entity, data) in query.iter() {
|
||||
let now = Instant::now();
|
||||
unsafe {
|
||||
let data = data.as_unsafe();
|
||||
let mut copy = Box::<UnsafeChunkData>::new_uninit();
|
||||
std::ptr::copy(data, copy.as_mut_ptr(), 1);
|
||||
state
|
||||
.sender
|
||||
.send(MeshRequest {
|
||||
time: Instant::now(),
|
||||
entity,
|
||||
voxels: copy.assume_init(),
|
||||
})
|
||||
.unwrap();
|
||||
commands.entity(entity).remove::<ChunkMeshDirty>();
|
||||
}
|
||||
let elapsed = now.elapsed();
|
||||
debug!("Created snapshot of {entity:?} in {:?}", elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
fn submit_meshed(mut commands: Commands, state: Res<ChunkMesherState>) {
|
||||
if state.receiver.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// let mut world = plugin_state.world.lock().unwrap();
|
||||
// let Some(mut renderer) = world.get_resource_mut::<ChunkRenderer>() else {
|
||||
// return;
|
||||
// };
|
||||
|
||||
// for MeshResult {
|
||||
// time,
|
||||
// entity,
|
||||
// faces,
|
||||
// } in state.receiver.try_iter()
|
||||
// {
|
||||
// commands.entity(entity).insert(ChunkMesh {
|
||||
// last_update: time,
|
||||
// range: renderer.set_mesh(entity, faces),
|
||||
// });
|
||||
// }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
const fn rotate_indices<const FACE: usize>([x, y, z]: [usize; 3]) -> [usize; 3] {
|
||||
const TOP: usize = FaceDirection::Top as usize;
|
||||
const BOTTOM: usize = FaceDirection::Bottom as usize;
|
||||
const LEFT: usize = FaceDirection::Left as usize;
|
||||
const RIGHT: usize = FaceDirection::Right as usize;
|
||||
const FRONT: usize = FaceDirection::Front as usize;
|
||||
const BACK: usize = FaceDirection::Back as usize;
|
||||
match FACE {
|
||||
TOP => todo!(),
|
||||
BOTTOM => todo!(),
|
||||
LEFT => todo!(),
|
||||
RIGHT => todo!(),
|
||||
FRONT => [x, y, z],
|
||||
BACK => todo!(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_chunk_mesh<const FACE: usize>(voxels: &UnsafeChunkData, faces: &mut Vec<Face>) {
|
||||
for z in 0..SIZE {
|
||||
for y in 0..SIZE {
|
||||
for x in 0..SIZE {
|
||||
if voxels[rotate_indices::<FACE>([x, y, z])] == 0 {
|
||||
continue;
|
||||
}
|
||||
if z != 0 && voxels[rotate_indices::<FACE>([x, y, z - 1])] != 0 {
|
||||
continue;
|
||||
}
|
||||
faces.push(
|
||||
Face::new()
|
||||
.with_x(x as _)
|
||||
.with_y(y as _)
|
||||
.with_colour(FaceColour::new().with_r(255)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+2
@@ -0,0 +1,2 @@
|
||||
pub mod mesh_gen;
|
||||
pub mod renderer;
|
||||
Executable
+283
@@ -0,0 +1,283 @@
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
use crate::state::{FrameResources, RenderState};
|
||||
use bevy_ecs::{
|
||||
entity::Entity,
|
||||
resource::Resource,
|
||||
system::{Res, ResMut},
|
||||
};
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use modular_bitfield::{Specifier, bitfield, specifiers::*};
|
||||
use rangemap::RangeSet;
|
||||
use tracing::error;
|
||||
use voxtech_terrain::chunk::ChunkId;
|
||||
use wgpu::{
|
||||
naga::FastHashMap,
|
||||
util::{BufferInitDescriptor, DeviceExt, RenderEncoder},
|
||||
*,
|
||||
};
|
||||
|
||||
#[derive(Specifier, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[bits = 3]
|
||||
pub enum FaceDirection {
|
||||
Top,
|
||||
Bottom,
|
||||
Left,
|
||||
Right,
|
||||
Front,
|
||||
Back,
|
||||
}
|
||||
|
||||
#[bitfield]
|
||||
#[derive(Specifier, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct FaceColour {
|
||||
pub r: u8,
|
||||
pub g: u8,
|
||||
pub b: u8,
|
||||
pub a: u8,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[bitfield]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Zeroable, Pod)]
|
||||
pub struct Face {
|
||||
pub face: FaceDirection,
|
||||
pub x: B5,
|
||||
pub y: B5,
|
||||
pub z: B5,
|
||||
pub w: B5,
|
||||
pub h: B5,
|
||||
pub meta: B4,
|
||||
pub colour: FaceColour,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, Zeroable, Pod)]
|
||||
pub struct Vertex {
|
||||
pub position: [f32; 2],
|
||||
}
|
||||
|
||||
impl Vertex {
|
||||
pub fn square() -> [Self; 4] {
|
||||
[
|
||||
Self {
|
||||
position: [0.0, 1.0],
|
||||
},
|
||||
Self {
|
||||
position: [0.0, 0.0],
|
||||
},
|
||||
Self {
|
||||
position: [1.0, 1.0],
|
||||
},
|
||||
Self {
|
||||
position: [1.0, 0.0],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
pub fn pipeline(device: &Device, surface_config: &SurfaceConfiguration) -> RenderPipeline {
|
||||
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: None,
|
||||
entries: &[BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: ShaderStages::VERTEX_FRAGMENT,
|
||||
ty: BindingType::Buffer {
|
||||
ty: BufferBindingType::Storage { read_only: true },
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
}],
|
||||
});
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: None,
|
||||
bind_group_layouts: &[Some(&bind_group_layout)],
|
||||
immediate_size: 0,
|
||||
});
|
||||
|
||||
let shader = device.create_shader_module(wgpu::include_wgsl!("shader.wgsl"));
|
||||
|
||||
let vertex_buffers = [wgpu::VertexBufferLayout {
|
||||
array_stride: size_of::<Self>() as wgpu::BufferAddress,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
attributes: &[wgpu::VertexAttribute {
|
||||
format: wgpu::VertexFormat::Float32x2,
|
||||
offset: 0,
|
||||
shader_location: 0,
|
||||
}],
|
||||
}];
|
||||
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("chunk_renderer"),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_main"),
|
||||
compilation_options: Default::default(),
|
||||
buffers: &vertex_buffers,
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_main"),
|
||||
compilation_options: Default::default(),
|
||||
targets: &[
|
||||
Some(surface_config.view_formats[0].into()),
|
||||
Some(TextureFormat::R32Uint.into()),
|
||||
],
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
cull_mode: Some(wgpu::Face::Back),
|
||||
topology: PrimitiveTopology::TriangleStrip,
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: None,
|
||||
multisample: wgpu::MultisampleState::default(),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct ChunkRenderer {
|
||||
vertex_buffer: Buffer,
|
||||
face_buffer: Arc<Buffer>,
|
||||
bind_group: BindGroup,
|
||||
pipeline: RenderPipeline,
|
||||
id_texture: Texture,
|
||||
free: RangeSet<usize>,
|
||||
used: FastHashMap<ChunkId, Range<usize>>,
|
||||
writes: Vec<(Range<u64>, Vec<Face>)>,
|
||||
}
|
||||
|
||||
impl ChunkRenderer {
|
||||
pub fn new(device: &Device, surface_config: &SurfaceConfiguration) -> Self {
|
||||
const MAX_FACE_COUNT: usize = 100000;
|
||||
let vertex_data = Vertex::square();
|
||||
let pipeline = Vertex::pipeline(device, surface_config);
|
||||
let vertex_buffer = device.create_buffer_init(&BufferInitDescriptor {
|
||||
label: Some("Vertex Buffer"),
|
||||
contents: bytemuck::cast_slice(&vertex_data),
|
||||
usage: BufferUsages::VERTEX,
|
||||
});
|
||||
let face_buffer = device.create_buffer(&BufferDescriptor {
|
||||
label: Some("Face Buffer"),
|
||||
size: (MAX_FACE_COUNT * size_of::<Face>()) as u64,
|
||||
mapped_at_creation: false,
|
||||
usage: BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
||||
});
|
||||
let bind_group = device.create_bind_group(&BindGroupDescriptor {
|
||||
label: Some("Chunk bindings"),
|
||||
layout: &pipeline.get_bind_group_layout(0),
|
||||
entries: &[BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: BindingResource::Buffer(BufferBinding {
|
||||
buffer: &face_buffer,
|
||||
offset: 0,
|
||||
size: None,
|
||||
}),
|
||||
}],
|
||||
});
|
||||
|
||||
Self {
|
||||
vertex_buffer,
|
||||
face_buffer: face_buffer.into(),
|
||||
pipeline,
|
||||
bind_group,
|
||||
id_texture: device.create_texture(&TextureDescriptor {
|
||||
label: Some("Chunk visibility texture"),
|
||||
size: Extent3d {
|
||||
width: surface_config.width,
|
||||
height: surface_config.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: TextureDimension::D2,
|
||||
format: TextureFormat::R32Uint,
|
||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[],
|
||||
}),
|
||||
free: RangeSet::from_iter([0..MAX_FACE_COUNT]),
|
||||
used: FastHashMap::default(),
|
||||
writes: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_mesh(&mut self, chunk: ChunkId, faces: Vec<Face>) -> Range<usize> {
|
||||
for range in self.free.iter() {
|
||||
if range.len() >= faces.len() {
|
||||
let mut range = range.clone();
|
||||
range.end = range.start + faces.len();
|
||||
self.free.remove(range.clone());
|
||||
self.used.entry(chunk).insert(range.clone());
|
||||
|
||||
let bounds = (range.start * size_of::<Face>()) as u64
|
||||
..(range.end * size_of::<Face>()) as u64;
|
||||
|
||||
self.writes.push((bounds, faces));
|
||||
return range;
|
||||
}
|
||||
}
|
||||
error!(
|
||||
"Could not find a suitable buffer range for entity {chunk:?}. The chunk will not be rendered."
|
||||
);
|
||||
if let Some(range) = self.used.remove(&chunk) {
|
||||
self.free.insert(range);
|
||||
}
|
||||
0..0
|
||||
}
|
||||
|
||||
pub(crate) fn render(
|
||||
mut renderer: ResMut<ChunkRenderer>,
|
||||
state: Res<RenderState>,
|
||||
mut frame: ResMut<FrameResources>,
|
||||
) {
|
||||
for (range, faces) in std::mem::take(&mut renderer.writes) {
|
||||
state.queue.write_buffer(
|
||||
&renderer.face_buffer,
|
||||
range.start,
|
||||
bytemuck::cast_slice(&faces),
|
||||
);
|
||||
}
|
||||
|
||||
let view = frame.screen_view.clone();
|
||||
let mut pass = frame
|
||||
.command_encoder
|
||||
.begin_render_pass(&RenderPassDescriptor {
|
||||
label: Some("Render triangle"),
|
||||
color_attachments: &[
|
||||
Some(RenderPassColorAttachment {
|
||||
view: &view,
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
ops: Operations {
|
||||
load: LoadOp::DontCare(LoadOpDontCare::default()),
|
||||
store: StoreOp::Store,
|
||||
},
|
||||
}),
|
||||
Some(RenderPassColorAttachment {
|
||||
view: &renderer
|
||||
.id_texture
|
||||
.create_view(&TextureViewDescriptor::default()),
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
ops: Operations {
|
||||
load: LoadOp::DontCare(LoadOpDontCare::default()),
|
||||
store: StoreOp::Store,
|
||||
},
|
||||
}),
|
||||
],
|
||||
depth_stencil_attachment: None,
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
pass.set_bind_group(0, &renderer.bind_group, &[]);
|
||||
pass.set_pipeline(&renderer.pipeline);
|
||||
pass.set_vertex_buffer(0, renderer.vertex_buffer.slice(..));
|
||||
for range in renderer.used.values() {
|
||||
pass.draw(0..4, range.start as u32..range.end as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
enable draw_index;
|
||||
|
||||
struct VertexInput {
|
||||
@location(0) position: vec2<f32>,
|
||||
@builtin(instance_index) draw_id: u32,
|
||||
};
|
||||
|
||||
struct VertexOutput {
|
||||
@location(0) albedo: vec4<f32>,
|
||||
@builtin(position) position: vec4<f32>,
|
||||
};
|
||||
|
||||
struct Face {
|
||||
transform: u32,
|
||||
colour: u32,
|
||||
};
|
||||
|
||||
@group(0) @binding(0)
|
||||
var<storage, read> faces: array<Face>;
|
||||
|
||||
@vertex
|
||||
fn vs_main(
|
||||
input: VertexInput
|
||||
) -> VertexOutput {
|
||||
let face = faces[input.draw_id];
|
||||
let transform = face.transform;
|
||||
let direction = transform & 0x07;
|
||||
let x = f32((transform & 0x00000F8) >> 3);
|
||||
let y = f32((transform & 0x0001F00) >> 8);
|
||||
let z = f32((transform & 0x003E000) >> 13);
|
||||
let w = f32((transform & 0x07C0000) >> 18);
|
||||
let h = f32((transform & 0xF800000) >> 23);
|
||||
|
||||
var result: VertexOutput;
|
||||
result.position = vec4(input.position.x + x, input.position.y + y, z, 1.0);
|
||||
result.position.x += w * f32(input.position.x != 0);
|
||||
result.position.y += h * f32(input.position.y != 0);
|
||||
result.position = vec4(result.position.xy * 0.1, result.position.zw);
|
||||
|
||||
result.albedo = vec4(
|
||||
f32(face.colour & 0x000000FF) / 255,
|
||||
f32((face.colour & 0x0000FF00) >> 8) / 255,
|
||||
f32((face.colour & 0x00FF0000) >> 16) / 255,
|
||||
f32((face.colour & 0xFF000000) >> 24) / 255,
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
struct FragmentOutput {
|
||||
@location(0) albedo: vec4<f32>,
|
||||
@location(1) chunk_id: u32,
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(vertex: VertexOutput) -> FragmentOutput {
|
||||
var output: FragmentOutput;
|
||||
output.albedo = vertex.albedo;
|
||||
output.chunk_id = 0;
|
||||
return output;
|
||||
}
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
pub mod chunk;
|
||||
pub mod plugin;
|
||||
pub mod state;
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Default,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Hash,
|
||||
bevy_ecs::schedule::ScheduleLabel,
|
||||
bevy_app::AppLabel,
|
||||
)]
|
||||
pub struct Render;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, bevy_ecs::message::Message)]
|
||||
pub struct WindowEvent(pub winit::event::WindowEvent);
|
||||
Executable
+140
@@ -0,0 +1,140 @@
|
||||
use crate::{
|
||||
Render, WindowEvent,
|
||||
chunk::{self, renderer::ChunkRenderer},
|
||||
state::RenderState,
|
||||
};
|
||||
use bevy_app::{App, Plugin, Update};
|
||||
use bevy_ecs::{resource::Resource, schedule::Schedule};
|
||||
use std::{
|
||||
ops::{Deref, DerefMut},
|
||||
sync::Arc,
|
||||
thread::JoinHandle,
|
||||
};
|
||||
use tracing::info;
|
||||
use winit::{
|
||||
application::ApplicationHandler,
|
||||
dpi::{PhysicalSize, Size},
|
||||
event::WindowEvent as WinitWindowEvent,
|
||||
event_loop::EventLoop,
|
||||
platform::wayland::EventLoopBuilderExtWayland,
|
||||
window::Window,
|
||||
};
|
||||
|
||||
pub struct RenderPlugin;
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Resource)]
|
||||
pub struct RenderPluginState {
|
||||
thread: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl RenderPluginState {
|
||||
pub fn is_running(&self) -> bool {
|
||||
!self.thread.is_finished()
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for RenderPlugin {
|
||||
fn build(&self, app: &mut bevy_app::App) {
|
||||
info!("Registering render plugin...");
|
||||
app.add_message::<WindowEvent>();
|
||||
|
||||
let thread = std::thread::spawn(move || {
|
||||
let mut app = App::new();
|
||||
app.add_systems(Update, RenderState::handle_window_events);
|
||||
app.add_schedule({
|
||||
let mut schedule = Schedule::new(Render);
|
||||
schedule.add_systems(ChunkRenderer::render);
|
||||
schedule
|
||||
});
|
||||
|
||||
let event_loop = EventLoop::builder().with_any_thread(true).build().unwrap();
|
||||
event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll);
|
||||
event_loop.run_app(&mut RenderApp(app)).unwrap();
|
||||
info!("Render thread stopped");
|
||||
});
|
||||
|
||||
app.insert_resource(RenderPluginState { thread });
|
||||
app.add_schedule(Schedule::new(Render));
|
||||
app.add_plugins(chunk::mesh_gen::ChunkMesher { thread_count: 1 });
|
||||
}
|
||||
|
||||
fn cleanup(&self, app: &mut bevy_app::App) {
|
||||
let state = app
|
||||
.world_mut()
|
||||
.remove_resource::<RenderPluginState>()
|
||||
.unwrap();
|
||||
|
||||
state.thread.join().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
struct RenderApp(App);
|
||||
|
||||
impl Deref for RenderApp {
|
||||
type Target = App;
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for RenderApp {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplicationHandler for RenderApp {
|
||||
fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
|
||||
info!("Creating render state...");
|
||||
let attributes = Window::default_attributes()
|
||||
.with_min_inner_size(Size::Physical(PhysicalSize {
|
||||
width: 1280,
|
||||
height: 720,
|
||||
}))
|
||||
.with_decorations(true)
|
||||
.with_visible(true);
|
||||
|
||||
let window = Arc::new(event_loop.create_window(attributes).unwrap());
|
||||
let state = pollster::block_on(RenderState::new(
|
||||
event_loop.owned_display_handle(),
|
||||
window.clone(),
|
||||
));
|
||||
self.insert_resource(ChunkRenderer::new(
|
||||
&state.device,
|
||||
&state.surface.get_configuration().unwrap(),
|
||||
));
|
||||
self.insert_resource(state);
|
||||
window.request_redraw();
|
||||
}
|
||||
|
||||
fn window_event(
|
||||
&mut self,
|
||||
event_loop: &winit::event_loop::ActiveEventLoop,
|
||||
_: winit::window::WindowId,
|
||||
event: WinitWindowEvent,
|
||||
) {
|
||||
let world = self.world_mut();
|
||||
|
||||
match event {
|
||||
WinitWindowEvent::CloseRequested => {
|
||||
info!("The close button was pressed; stopping");
|
||||
world.remove_resource::<RenderState>();
|
||||
event_loop.exit();
|
||||
}
|
||||
|
||||
WinitWindowEvent::RedrawRequested => {
|
||||
world.run_schedule(Update);
|
||||
world.run_system_cached(RenderState::frame_begin).unwrap();
|
||||
world.run_schedule(Render);
|
||||
world.run_system_cached(RenderState::frame_present).unwrap();
|
||||
}
|
||||
|
||||
_ => {
|
||||
world.write_message(WindowEvent(event.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+183
@@ -0,0 +1,183 @@
|
||||
use bevy_ecs::{
|
||||
message::MessageReader, resource::Resource, schedule::ScheduleLabel, system::ResMut,
|
||||
world::World,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use wgpu::*;
|
||||
use winit::{event_loop::OwnedDisplayHandle, window::Window};
|
||||
|
||||
use crate::WindowEvent;
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct RenderState {
|
||||
pub(crate) instance: Instance,
|
||||
pub(crate) window: Arc<Window>,
|
||||
pub(crate) device: Device,
|
||||
pub(crate) queue: Queue,
|
||||
pub(crate) size: winit::dpi::PhysicalSize<u32>,
|
||||
pub(crate) surface: Surface<'static>,
|
||||
pub(crate) surface_format: TextureFormat,
|
||||
}
|
||||
|
||||
impl RenderState {
|
||||
pub async fn new(display: OwnedDisplayHandle, window: Arc<Window>) -> Self {
|
||||
let instance = Instance::new({
|
||||
let mut descriptor = InstanceDescriptor::new_with_display_handle(Box::new(display));
|
||||
descriptor.backends = Backends::VULKAN | Backends::GL;
|
||||
descriptor
|
||||
});
|
||||
let adapter = instance
|
||||
.request_adapter(&RequestAdapterOptions::default())
|
||||
.await
|
||||
.unwrap();
|
||||
let (device, queue) = adapter
|
||||
.request_device(&DeviceDescriptor {
|
||||
required_features: Features::MULTI_DRAW_INDIRECT_COUNT
|
||||
| Features::SHADER_DRAW_INDEX,
|
||||
..DeviceDescriptor::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 mut state = Self {
|
||||
instance,
|
||||
window,
|
||||
device,
|
||||
queue,
|
||||
size,
|
||||
surface,
|
||||
surface_format,
|
||||
};
|
||||
|
||||
state.configure_surface();
|
||||
state
|
||||
}
|
||||
|
||||
pub fn window(&self) -> &Window {
|
||||
&self.window
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
|
||||
self.size = new_size;
|
||||
self.configure_surface();
|
||||
}
|
||||
|
||||
fn configure_surface(&mut self) {
|
||||
let surface_config = SurfaceConfiguration {
|
||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||
format: self.surface_format,
|
||||
view_formats: vec![self.surface_format.add_srgb_suffix()],
|
||||
alpha_mode: CompositeAlphaMode::Auto,
|
||||
width: self.size.width,
|
||||
height: self.size.height,
|
||||
desired_maximum_frame_latency: 2,
|
||||
present_mode: PresentMode::AutoVsync,
|
||||
};
|
||||
self.surface.configure(&self.device, &surface_config);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, ScheduleLabel)]
|
||||
pub enum RenderPhase {
|
||||
Begin,
|
||||
Draw,
|
||||
Present,
|
||||
}
|
||||
|
||||
pub trait RenderPass {
|
||||
fn pipeline_id(&self) -> &str;
|
||||
fn draw(&self, encoder: &mut CommandEncoder, texture_view: &TextureView);
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct FrameResources {
|
||||
pub screen_view: TextureView,
|
||||
pub screen_texture: SurfaceTexture,
|
||||
pub command_encoder: CommandEncoder,
|
||||
}
|
||||
|
||||
impl RenderState {
|
||||
pub(crate) fn frame_begin(world: &mut World) {
|
||||
let mut state = world.resource_mut::<RenderState>();
|
||||
let surface_texture = match state.surface.get_current_texture() {
|
||||
CurrentSurfaceTexture::Success(texture) => texture,
|
||||
CurrentSurfaceTexture::Occluded | CurrentSurfaceTexture::Timeout => return,
|
||||
CurrentSurfaceTexture::Suboptimal(texture) => {
|
||||
drop(texture);
|
||||
state.configure_surface();
|
||||
return;
|
||||
}
|
||||
CurrentSurfaceTexture::Outdated => {
|
||||
state.configure_surface();
|
||||
return;
|
||||
}
|
||||
CurrentSurfaceTexture::Validation => {
|
||||
unreachable!("No error scope registered, so validation errors will panic")
|
||||
}
|
||||
CurrentSurfaceTexture::Lost => {
|
||||
state.surface = state.instance.create_surface(state.window.clone()).unwrap();
|
||||
state.configure_surface();
|
||||
return;
|
||||
}
|
||||
};
|
||||
let texture_view = surface_texture.texture.create_view(&TextureViewDescriptor {
|
||||
format: Some(state.surface_format.add_srgb_suffix()),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let mut encoder = state.device.create_command_encoder(&Default::default());
|
||||
{
|
||||
let _clear_screen_pass = encoder.begin_render_pass(&RenderPassDescriptor {
|
||||
label: None,
|
||||
color_attachments: &[Some(RenderPassColorAttachment {
|
||||
view: &texture_view,
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
ops: Operations {
|
||||
load: LoadOp::Clear(Color::BLACK),
|
||||
store: StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
}
|
||||
|
||||
world.insert_resource(FrameResources {
|
||||
screen_texture: surface_texture,
|
||||
screen_view: texture_view,
|
||||
command_encoder: encoder,
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn frame_present(world: &mut World) {
|
||||
let resources = world.remove_resource::<FrameResources>().unwrap();
|
||||
let state = world.resource_ref::<RenderState>();
|
||||
state.queue.submit([resources.command_encoder.finish()]);
|
||||
state.window.pre_present_notify();
|
||||
resources.screen_texture.present();
|
||||
state.window().request_redraw();
|
||||
}
|
||||
|
||||
pub(crate) fn handle_window_events(
|
||||
mut reader: MessageReader<WindowEvent>,
|
||||
mut state: ResMut<RenderState>,
|
||||
) {
|
||||
for event in reader.read() {
|
||||
match event.0 {
|
||||
winit::event::WindowEvent::Resized(size) => {
|
||||
state.resize(size);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "voxtech_terrain"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
bevy_ecs = "0.18.1"
|
||||
bevy_app = "0.18.1"
|
||||
modular-bitfield = "0.13.1"
|
||||
Executable
+93
@@ -0,0 +1,93 @@
|
||||
use std::{
|
||||
ops::{Deref, DerefMut, Index, IndexMut},
|
||||
sync::{Arc, atomic::AtomicU32},
|
||||
};
|
||||
|
||||
use bevy_ecs::component::Component;
|
||||
use modular_bitfield::{
|
||||
bitfield,
|
||||
specifiers::{B1, B21},
|
||||
};
|
||||
|
||||
pub const SIZE: usize = 32;
|
||||
pub const SIZE2: usize = SIZE * SIZE;
|
||||
pub const SIZE3: usize = SIZE * SIZE2;
|
||||
|
||||
#[bitfield]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Component)]
|
||||
pub struct ChunkId {
|
||||
x: B21,
|
||||
y: B21,
|
||||
z: B21,
|
||||
b: B1,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Component)]
|
||||
pub struct Chunk;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct ChunkData(Arc<[AtomicU32; SIZE3]>);
|
||||
|
||||
impl ChunkData {
|
||||
pub fn empty() -> Self {
|
||||
unsafe { Self(Arc::new_zeroed().assume_init()) }
|
||||
}
|
||||
|
||||
pub fn solid(voxel_id: u32) -> Self {
|
||||
let mut data = Arc::new_uninit();
|
||||
let inner = Arc::get_mut(&mut data).unwrap();
|
||||
let ptr = inner.as_mut_ptr() as *mut u32;
|
||||
unsafe {
|
||||
for i in 0..SIZE {
|
||||
ptr.add(i).write(voxel_id);
|
||||
}
|
||||
Self(data.assume_init())
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub unsafe fn as_unsafe(&self) -> &UnsafeChunkData {
|
||||
unsafe { &*(self.0.as_ptr() as *const UnsafeChunkData) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<[usize; 3]> for ChunkData {
|
||||
type Output = AtomicU32;
|
||||
#[inline]
|
||||
fn index(&self, [x, y, z]: [usize; 3]) -> &Self::Output {
|
||||
&self.0[x + y * SIZE + z * SIZE2]
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UnsafeChunkData([u32; SIZE3]);
|
||||
|
||||
impl Deref for UnsafeChunkData {
|
||||
type Target = [u32; SIZE3];
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for UnsafeChunkData {
|
||||
#[inline]
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<[usize; 3]> for UnsafeChunkData {
|
||||
type Output = u32;
|
||||
#[inline]
|
||||
fn index(&self, [x, y, z]: [usize; 3]) -> &Self::Output {
|
||||
&self.0[x + y * SIZE + z * SIZE2]
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexMut<[usize; 3]> for UnsafeChunkData {
|
||||
#[inline]
|
||||
fn index_mut(&mut self, [x, y, z]: [usize; 3]) -> &mut Self::Output {
|
||||
&mut self.0[x + y * SIZE + z * SIZE2]
|
||||
}
|
||||
}
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
pub mod chunk;
|
||||
Reference in New Issue
Block a user