Spinning Cube

This commit is contained in:
2026-02-11 01:45:25 +01:00
parent 77ed489f22
commit 1f689bab03
6 changed files with 559 additions and 0 deletions

View File

@@ -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<f32>;
@group(0) @binding(1) var texture_sampler: sampler;
@group(0) @binding(2) var<uniform> settings: PostProcessSettings;
const dither = array<array<f32, 8>, 8>(
array<f32, 8>( 0.0, 32.0, 8.0, 40.0, 2.0, 34.0, 10.0, 42.0),
array<f32, 8>(48.0, 16.0, 56.0, 24.0, 50.0, 18.0, 58.0, 26.0),
array<f32, 8>(12.0, 44.0, 4.0, 36.0, 14.0, 46.0, 6.0, 38.0),
array<f32, 8>(60.0, 28.0, 52.0, 20.0, 62.0, 30.0, 54.0, 22.0),
array<f32, 8>( 3.0, 35.0, 11.0, 43.0, 1.0, 33.0, 9.0, 41.0),
array<f32, 8>(51.0, 19.0, 59.0, 27.0, 49.0, 17.0, 57.0, 25.0),
array<f32, 8>(15.0, 47.0, 7.0, 39.0, 13.0, 45.0, 5.0, 37.0),
array<f32, 8>(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<f32> {
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<f32>(
final_gray, final_gray, final_gray, a
);
}

View File

@@ -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::<PostProcessSettings>::default(),
UniformComponentPlugin::<PostProcessSettings>::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::<ViewNodeRunner<PostProcessNode>>(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<PostProcessSettings>,
);
fn run(
&self,
_graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
(view_target, _post_process_settings, settings_index): QueryItem<Self::ViewQuery>,
world: &World,
) -> Result<(), NodeRunError> {
let post_process_pipeline = world.resource::<PostProcessPipeline>();
let pipeline_cache = world.resource::<PipelineCache>();
let Some(pipeline) = pipeline_cache.get_render_pipeline(post_process_pipeline.pipeline_id)
else {
return Ok(());
};
let settings_uniforms = world.resource::<ComponentUniforms<PostProcessSettings>>();
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<RenderDevice>,
asset_server: Res<AssetServer>,
fullscreen_shader: Res<FullscreenShader>,
pipeline_cache: Res<PipelineCache>,
) {
let layout = BindGroupLayoutDescriptor::new(
"post_process_bind_group_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
texture_2d(TextureSampleType::Float { filterable: true }),
sampler(SamplerBindingType::Filtering),
uniform_buffer::<PostProcessSettings>(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,
}

View File

@@ -0,0 +1,8 @@
mod node;
mod plugin;
#[allow(unused)]
pub use plugin::{
G13Resource, GpuImageExportSource, ImageExport, ImageExportPlugin, ImageExportSource,
ImageExportSystems,
};

View File

@@ -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::<RenderAssets<GpuImageExportSource>>()
.iter()
{
if let Some(gpu_image) = world
.resource::<RenderAssets<GpuImage>>()
.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(())
}
}

View File

@@ -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<Image>);
impl From<Handle<Image>> for ImageExportSource {
fn from(value: Handle<Image>) -> Self {
Self(value)
}
}
pub struct GpuImageExportSource {
pub buffer: Buffer,
pub source_handle: Handle<Image>,
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<RenderDevice>, SRes<RenderAssets<GpuImage>>);
fn asset_usage(_: &Self::SourceAsset) -> RenderAssetUsages {
RenderAssetUsages::default()
}
fn prepare_asset(
source_asset: Self::SourceAsset,
_asset_id: AssetId<Self::SourceAsset>,
(device, images): &mut SystemParamItem<Self::Param>,
_previous_asset: Option<&Self>,
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
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<usize> {
None
}
}
#[derive(Component, ExtractComponent, Clone, Default, Debug)]
pub struct ImageExport(pub Handle<ImageExportSource>);
fn save_buffer_to_disk(
export_bundles: Query<&ImageExport>,
sources: Res<RenderAssets<GpuImageExportSource>>,
render_device: Res<RenderDevice>,
mut g13: ResMut<G13Resource>,
) {
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::<u8>::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::<BinaryColor>::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::<ImageExportSource>()
.init_asset::<ImageExportSource>()
.register_asset_reflect::<ImageExportSource>()
.add_plugins((
RenderAssetPlugin::<GpuImageExportSource>::default(),
ExtractComponentPlugin::<ImageExport>::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::<RenderGraph>()
.unwrap();
graph.add_node(ImageExportLabel, ImageExportNode);
graph.add_node_edge(CameraDriverLabel, ImageExportLabel);
}
}

47
mini-game/src/shared.rs Normal file
View File

@@ -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<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
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<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
commands.spawn((
Spinner,
Mesh2d(meshes.add(RegularPolygon::new(66.0, 8))),
MeshMaterial2d(materials.add(Color::srgb(0.4, 0.4, 0.6))),
));
}