diff --git a/mini-game/assets/shaders/post_processing.wgsl b/mini-game/assets/shaders/post_processing.wgsl new file mode 100644 index 0000000..f912dff --- /dev/null +++ b/mini-game/assets/shaders/post_processing.wgsl @@ -0,0 +1,59 @@ +#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput + +struct PostProcessSettings { + scale: f32, +} + +@group(0) @binding(0) var screen_texture: texture_2d; +@group(0) @binding(1) var texture_sampler: sampler; +@group(0) @binding(2) var settings: PostProcessSettings; + +const dither = array, 8>( + array( 0.0, 32.0, 8.0, 40.0, 2.0, 34.0, 10.0, 42.0), + array(48.0, 16.0, 56.0, 24.0, 50.0, 18.0, 58.0, 26.0), + array(12.0, 44.0, 4.0, 36.0, 14.0, 46.0, 6.0, 38.0), + array(60.0, 28.0, 52.0, 20.0, 62.0, 30.0, 54.0, 22.0), + array( 3.0, 35.0, 11.0, 43.0, 1.0, 33.0, 9.0, 41.0), + array(51.0, 19.0, 59.0, 27.0, 49.0, 17.0, 57.0, 25.0), + array(15.0, 47.0, 7.0, 39.0, 13.0, 45.0, 5.0, 37.0), + array(63.0, 31.0, 55.0, 23.0, 61.0, 29.0, 53.0, 21.0) +); + +fn find_closest(x: i32, y: i32, c0: f32) -> f32 { + + + var limit = 0.0; + + if (x < 8) { + limit = (dither[x][y]+1) / 64.0; + } + + if (c0 < limit) { + return 0.0; + } + + return 1.0; +} + +@fragment +fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { + let a: f32 = textureSample(screen_texture, texture_sampler, in.uv).a; + + let gr: f32 = textureSample(screen_texture, texture_sampler, in.uv).r * 0.2989; + let gg: f32 = textureSample(screen_texture, texture_sampler, in.uv).g * 0.587; + let gb: f32 = textureSample(screen_texture, texture_sampler, in.uv).b * 0.114; + let grayscale: f32 = gr + gg + gb; + + let uvx: f32 = in.position.x; + let uvy: f32 = in.position.y; + + let x: i32 = i32(uvx * settings.scale) % 8; + let y: i32 = i32(uvy * settings.scale) % 8; + + let final_gray = find_closest(x, y, grayscale); /// not even used?! + + // Sample each color channel with an arbitrary shift + return vec4( + final_gray, final_gray, final_gray, a + ); +} diff --git a/mini-game/src/post_process.rs b/mini-game/src/post_process.rs new file mode 100644 index 0000000..b62bce4 --- /dev/null +++ b/mini-game/src/post_process.rs @@ -0,0 +1,182 @@ +use bevy::core_pipeline::core_3d::graph::Core3d; +use bevy::ecs::query::QueryItem; +use bevy::prelude::*; +use bevy::render::extract_component::{ + DynamicUniformIndex, ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin, +}; +use bevy::render::render_graph::{ + NodeRunError, RenderGraphContext, RenderLabel, ViewNode, ViewNodeRunner, +}; +use bevy::render::render_resource::{ + BindGroupEntries, PipelineCache, RenderPipelineDescriptor, ShaderType, TextureFormat, +}; +use bevy::render::renderer::RenderContext; +use bevy::render::view::ViewTarget; +use bevy::render::{RenderApp, RenderStartup}; + +use bevy::{ + core_pipeline::{FullscreenShader, core_3d::graph::Node3d}, + render::{ + extract_component::ComponentUniforms, + render_graph::RenderGraphExt, + render_resource::{ + binding_types::{sampler, texture_2d, uniform_buffer}, + *, + }, + renderer::RenderDevice, + }, +}; + +const SHADER_ASSET_PATH: &str = "shaders/post_processing.wgsl"; + +/// It is generally encouraged to set up post processing effects as a plugin +pub struct PostProcessPlugin; + +impl Plugin for PostProcessPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(( + ExtractComponentPlugin::::default(), + UniformComponentPlugin::::default(), + )); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app.add_systems(RenderStartup, init_post_process_pipeline); + + render_app + .add_render_graph_node::>(Core3d, PostProcessLabel) + .add_render_graph_edges( + Core3d, + ( + Node3d::Tonemapping, + PostProcessLabel, + Node3d::EndMainPassPostProcessing, + ), + ); + } +} + +#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)] +struct PostProcessLabel; + +#[derive(Default)] +struct PostProcessNode; + +impl ViewNode for PostProcessNode { + type ViewQuery = ( + &'static ViewTarget, + &'static PostProcessSettings, + &'static DynamicUniformIndex, + ); + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + (view_target, _post_process_settings, settings_index): QueryItem, + world: &World, + ) -> Result<(), NodeRunError> { + let post_process_pipeline = world.resource::(); + let pipeline_cache = world.resource::(); + let Some(pipeline) = pipeline_cache.get_render_pipeline(post_process_pipeline.pipeline_id) + else { + return Ok(()); + }; + + let settings_uniforms = world.resource::>(); + let Some(settings_binding) = settings_uniforms.uniforms().binding() else { + return Ok(()); + }; + + let post_process = view_target.post_process_write(); + + let bind_group = render_context.render_device().create_bind_group( + "post_process_bind_group", + &pipeline_cache.get_bind_group_layout(&post_process_pipeline.layout), + &BindGroupEntries::sequential(( + post_process.source, + &post_process_pipeline.sampler, + settings_binding.clone(), + )), + ); + + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("post_process_pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view: post_process.destination, + depth_slice: None, + resolve_target: None, + ops: Operations::default(), + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + render_pass.set_render_pipeline(pipeline); + render_pass.set_bind_group(0, &bind_group, &[settings_index.index()]); + render_pass.draw(0..3, 0..1); + + Ok(()) + } +} + +#[derive(Resource)] +struct PostProcessPipeline { + layout: BindGroupLayoutDescriptor, + sampler: Sampler, + pipeline_id: CachedRenderPipelineId, +} + +fn init_post_process_pipeline( + mut commands: Commands, + render_device: Res, + asset_server: Res, + fullscreen_shader: Res, + pipeline_cache: Res, +) { + let layout = BindGroupLayoutDescriptor::new( + "post_process_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + texture_2d(TextureSampleType::Float { filterable: true }), + sampler(SamplerBindingType::Filtering), + uniform_buffer::(true), + ), + ), + ); + + let sampler = render_device.create_sampler(&SamplerDescriptor::default()); + + let shader = asset_server.load(SHADER_ASSET_PATH); + let vertex_state = fullscreen_shader.to_vertex_state(); + let pipeline_id = pipeline_cache.queue_render_pipeline(RenderPipelineDescriptor { + label: Some("post_process_pipeline".into()), + layout: vec![layout.clone()], + vertex: vertex_state, + fragment: Some(FragmentState { + shader, + targets: vec![Some(ColorTargetState { + format: TextureFormat::bevy_default(), + blend: None, + write_mask: ColorWrites::ALL, + })], + ..default() + }), + ..default() + }); + + commands.insert_resource(PostProcessPipeline { + layout, + sampler, + pipeline_id, + }); +} + +#[derive(Component, Default, Clone, Copy, ExtractComponent, ShaderType)] +pub struct PostProcessSettings { + pub scale: f32, +} diff --git a/mini-game/src/renderer/mod.rs b/mini-game/src/renderer/mod.rs new file mode 100644 index 0000000..f89d984 --- /dev/null +++ b/mini-game/src/renderer/mod.rs @@ -0,0 +1,8 @@ +mod node; +mod plugin; + +#[allow(unused)] +pub use plugin::{ + G13Resource, GpuImageExportSource, ImageExport, ImageExportPlugin, ImageExportSource, + ImageExportSystems, +}; diff --git a/mini-game/src/renderer/node.rs b/mini-game/src/renderer/node.rs new file mode 100644 index 0000000..60fb023 --- /dev/null +++ b/mini-game/src/renderer/node.rs @@ -0,0 +1,49 @@ +use super::GpuImageExportSource; +use bevy::{ + prelude::*, + render::{ + render_asset::RenderAssets, + render_graph::{Node, NodeRunError, RenderGraphContext, RenderLabel}, + renderer::RenderContext, + texture::GpuImage, + }, +}; +use wgpu::{TexelCopyBufferInfo, TexelCopyBufferLayout}; + +#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)] +pub struct ImageExportLabel; + +pub struct ImageExportNode; +impl Node for ImageExportNode { + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + for (_, source) in world + .resource::>() + .iter() + { + if let Some(gpu_image) = world + .resource::>() + .get(&source.source_handle) + { + render_context.command_encoder().copy_texture_to_buffer( + gpu_image.texture.as_image_copy(), + TexelCopyBufferInfo { + buffer: &source.buffer, + layout: TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(source.padded_bytes_per_row), + rows_per_image: None, + }, + }, + source.source_size, + ); + } + } + + Ok(()) + } +} diff --git a/mini-game/src/renderer/plugin.rs b/mini-game/src/renderer/plugin.rs new file mode 100644 index 0000000..a58df72 --- /dev/null +++ b/mini-game/src/renderer/plugin.rs @@ -0,0 +1,214 @@ +use super::node::{ImageExportLabel, ImageExportNode}; +use bevy::{ + asset::RenderAssetUsages, + ecs::system::{SystemParamItem, lifetimeless::SRes}, + prelude::*, + render::{ + Render, RenderApp, RenderSystems, + extract_component::{ExtractComponent, ExtractComponentPlugin}, + graph::CameraDriverLabel, + render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets}, + render_graph::RenderGraph, + render_resource::{Buffer, BufferDescriptor, BufferUsages, Extent3d, MapMode}, + renderer::RenderDevice, + texture::GpuImage, + }, +}; +use embedded_graphics::pixelcolor::BinaryColor; +use futures::channel::oneshot; +use g13_driver::{G13, G13_LCD_BUF_SIZE}; +use wgpu::PollType; + +#[derive(Resource, Clone)] +pub struct G13Resource { + pub g13: G13, +} + +#[derive(Asset, Reflect, Clone, Default)] +pub struct ImageExportSource(pub Handle); + +impl From> for ImageExportSource { + fn from(value: Handle) -> Self { + Self(value) + } +} + +pub struct GpuImageExportSource { + pub buffer: Buffer, + pub source_handle: Handle, + pub source_size: Extent3d, + pub bytes_per_row: u32, + pub padded_bytes_per_row: u32, +} + +impl RenderAsset for GpuImageExportSource { + type SourceAsset = ImageExportSource; + type Param = (SRes, SRes>); + + fn asset_usage(_: &Self::SourceAsset) -> RenderAssetUsages { + RenderAssetUsages::default() + } + + fn prepare_asset( + source_asset: Self::SourceAsset, + _asset_id: AssetId, + (device, images): &mut SystemParamItem, + _previous_asset: Option<&Self>, + ) -> Result> { + let Some(gpu_image) = images.get(&source_asset.0) else { + return Err(PrepareAssetError::RetryNextUpdate(source_asset)); + }; + + let size = gpu_image.texture.size(); + let format = &gpu_image.texture_format; + let bytes_per_row = + (size.width / format.block_dimensions().0) * format.block_copy_size(None).unwrap(); + let padded_bytes_per_row = + RenderDevice::align_copy_bytes_per_row(bytes_per_row as usize) as u32; + + let source_size = gpu_image.texture.size(); + + Ok(GpuImageExportSource { + buffer: device.create_buffer(&BufferDescriptor { + label: Some("Image Export Buffer"), + size: (source_size.height * padded_bytes_per_row) as u64, + usage: BufferUsages::COPY_DST | BufferUsages::MAP_READ, + mapped_at_creation: false, + }), + source_handle: source_asset.0, + source_size, + bytes_per_row, + padded_bytes_per_row, + }) + } + + fn byte_len(_: &Self::SourceAsset) -> Option { + None + } +} + +#[derive(Component, ExtractComponent, Clone, Default, Debug)] +pub struct ImageExport(pub Handle); + +fn save_buffer_to_disk( + export_bundles: Query<&ImageExport>, + sources: Res>, + render_device: Res, + mut g13: ResMut, +) { + for export in &export_bundles { + if let Some(gpu_source) = sources.get(&export.0) { + let mut image_bytes = { + let slice = gpu_source.buffer.slice(..); + + { + let (mapping_tx, mapping_rx) = oneshot::channel(); + + render_device.map_buffer(&slice, MapMode::Read, move |res| { + mapping_tx.send(res).unwrap(); + }); + + if render_device + .poll(PollType::Wait { + submission_index: None, + timeout: None, + }) + .is_err() + { + break; + } + + futures_lite::future::block_on(mapping_rx).unwrap().unwrap(); + } + + slice.get_mapped_range().to_vec() + }; + + gpu_source.buffer.unmap(); + + let bytes_per_row = gpu_source.bytes_per_row as usize; + let padded_bytes_per_row = gpu_source.padded_bytes_per_row as usize; + let source_size = gpu_source.source_size; + + if bytes_per_row != padded_bytes_per_row { + let mut unpadded_bytes = + Vec::::with_capacity(source_size.height as usize * bytes_per_row); + + for padded_row in image_bytes.chunks(padded_bytes_per_row) { + unpadded_bytes.extend_from_slice(&padded_row[..bytes_per_row]); + } + + image_bytes = unpadded_bytes; + } + + let mut img_buffer = [0u8; G13_LCD_BUF_SIZE as usize]; + + for (i, b) in image_bytes + .iter() + .step_by(4) + .take(G13_LCD_BUF_SIZE as usize * 8) + .enumerate() + { + let offset = i / 8; + let mask = 1 << (7 - (i % 8)); + + if *b == 0xFF { + img_buffer[offset] |= mask; + } else { + img_buffer[offset] &= !mask; + } + } + + use embedded_graphics::image::ImageDrawable; + embedded_graphics::image::ImageRaw::::new(&img_buffer, 160) + .draw(&mut g13.g13) + .expect("G13 to be connected"); + + let _ = g13.g13.render(); + } + } +} + +/// Plugin enabling the generation of image sequences. +#[derive(Default)] +pub struct ImageExportPlugin; + +#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] +pub enum ImageExportSystems { + ImageExportSetup, +} + +impl Plugin for ImageExportPlugin { + fn build(&self, app: &mut App) { + use ImageExportSystems::*; + + let g13 = G13::new().expect("G13 to be connected"); + + app.configure_sets(PostUpdate, ImageExportSetup) + .register_type::() + .init_asset::() + .register_asset_reflect::() + .add_plugins(( + RenderAssetPlugin::::default(), + ExtractComponentPlugin::::default(), + )); + // .add_systems(PostUpdate, setup_exporters.in_set(ImageExportSetup)); + + let render_app = app.sub_app_mut(RenderApp); + + render_app.insert_resource(G13Resource { g13 }).add_systems( + Render, + save_buffer_to_disk + .after(RenderSystems::Render) + .before(RenderSystems::Cleanup), + ); + + let mut graph = render_app + .world_mut() + .get_resource_mut::() + .unwrap(); + + graph.add_node(ImageExportLabel, ImageExportNode); + graph.add_node_edge(CameraDriverLabel, ImageExportLabel); + } +} diff --git a/mini-game/src/shared.rs b/mini-game/src/shared.rs new file mode 100644 index 0000000..e73ac54 --- /dev/null +++ b/mini-game/src/shared.rs @@ -0,0 +1,47 @@ +use bevy::prelude::*; + +#[allow(dead_code)] +#[derive(Component)] +pub struct Spinner; + +#[allow(dead_code)] +#[derive(Resource, Default)] +pub struct Flags { + pub debug: bool, +} + +#[allow(dead_code)] +pub fn spawn_3d_scene( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + commands.spawn(( + Mesh3d(meshes.add(Cuboid::default())), + MeshMaterial3d(materials.add(Color::from(bevy::color::palettes::css::WHITE))), + Transform::from_xyz(0.0, 0.0, 0.0), + Spinner, + )); + + commands.spawn(( + PointLight { + intensity: 6_000_000., + shadows_enabled: true, + ..Default::default() + }, + Transform::from_xyz(3., 4., 6.), + )); +} + +#[allow(dead_code)] +pub fn spawn_2d_scene( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + commands.spawn(( + Spinner, + Mesh2d(meshes.add(RegularPolygon::new(66.0, 8))), + MeshMaterial2d(materials.add(Color::srgb(0.4, 0.4, 0.6))), + )); +}