Add pressure visual modes and breakout game

This commit is contained in:
lennlouisgeek
2026-06-29 03:13:57 +08:00
parent eab24fc2bf
commit f30ebcf20b
12 changed files with 1739 additions and 165 deletions

View File

@@ -68,6 +68,23 @@ const HAND_TIP_MATRICES: [HandTipMatrix; 5] = [
},
];
const HAND_PALM_CHIPS: [HandPalmChip; 2] = [
HandPalmChip {
center_px: [538.0, 608.0],
size_px: [248.0, 82.0],
angle_rad: 0.06,
rows: 5,
cols: 14,
},
HandPalmChip {
center_px: [606.0, 780.0],
size_px: [72.0, 214.0],
angle_rad: 0.05,
rows: 11,
cols: 4,
},
];
// Each entry pins one miniature matrix to a fingertip in hand.png.
// Coordinates are authored in source-image pixels so they are easy to tune by eye.
// size_px keeps the same 7:12 aspect as the Finger-mode 7 columns x 12 rows matrix.
@@ -77,6 +94,16 @@ struct HandTipMatrix {
angle_rad: f32,
}
// Palm chips follow the hand layout: one horizontal 5x14 matrix and one vertical
// 11x4 matrix, rendered as dark inset chip tiles on the palm.
struct HandPalmChip {
center_px: [f32; 2],
size_px: [f32; 2],
angle_rad: f32,
rows: u32,
cols: u32,
}
impl egui_wgpu::CallbackTrait for WgpuBackgroundCallback {
fn prepare(
&self,
@@ -117,6 +144,8 @@ pub struct BackgroundRenderResources {
dot_pipeline: wgpu::RenderPipeline,
hand_membrane_pipeline: wgpu::RenderPipeline,
hand_dot_pipeline: wgpu::RenderPipeline,
hand_palm_chip_pipeline: wgpu::RenderPipeline,
hand_palm_dot_pipeline: wgpu::RenderPipeline,
hand_image_bind_group: wgpu::BindGroup,
hand_image_texture: texture::Texture,
@@ -127,6 +156,10 @@ pub struct BackgroundRenderResources {
hand_membrane_instances: Vec<GlyphInstance>,
hand_dot_instance_buffer: wgpu::Buffer,
hand_dot_instances: Vec<GlyphInstance>,
hand_palm_chip_instance_buffer: wgpu::Buffer,
hand_palm_chip_instances: Vec<GlyphInstance>,
hand_palm_dot_instance_buffer: wgpu::Buffer,
hand_palm_dot_instances: Vec<GlyphInstance>,
render_options: RenderOptions,
}
@@ -452,6 +485,10 @@ impl BackgroundRenderResources {
create_hand_membrane_pipeline(device, target_format, &shader, &pipeline_layout);
let hand_dot_pipeline =
create_hand_dot_pipeline(device, target_format, &shader, &pipeline_layout);
let hand_palm_chip_pipeline =
create_hand_palm_chip_pipeline(device, target_format, &shader, &pipeline_layout);
let hand_palm_dot_pipeline =
create_hand_palm_dot_pipeline(device, target_format, &shader, &pipeline_layout);
let _model_pipelines =
create_model_pipelines(device, target_format, &shader, &model_pipeline_layout);
@@ -502,6 +539,29 @@ impl BackgroundRenderResources {
contents: bytemuck::cast_slice(&hand_dot_instances),
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
});
let hand_palm_chip_instances = build_hand_palm_chip_instances(
hand_image_texture.width as f32,
hand_image_texture.height as f32,
);
let hand_palm_chip_instance_buffer =
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Hand Palm Chip Instance Buffer"),
contents: bytemuck::cast_slice(&hand_palm_chip_instances),
usage: wgpu::BufferUsages::VERTEX,
});
let hand_palm_dot_instances = build_hand_palm_dot_instances(
rows,
cols,
hand_image_texture.width as f32,
hand_image_texture.height as f32,
&[[0.0, 0.0]; PRESSURE_CELL_COUNT],
);
let hand_palm_dot_instance_buffer =
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Hand Palm Chip Dot Instance Buffer"),
contents: bytemuck::cast_slice(&hand_palm_dot_instances),
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
});
Self {
layout,
@@ -517,6 +577,8 @@ impl BackgroundRenderResources {
dot_pipeline,
hand_membrane_pipeline,
hand_dot_pipeline,
hand_palm_chip_pipeline,
hand_palm_dot_pipeline,
hand_image_bind_group,
hand_image_texture,
glyph_vertex_buffer,
@@ -526,6 +588,10 @@ impl BackgroundRenderResources {
hand_membrane_instances,
hand_dot_instance_buffer,
hand_dot_instances,
hand_palm_chip_instance_buffer,
hand_palm_chip_instances,
hand_palm_dot_instance_buffer,
hand_palm_dot_instances,
render_options,
}
}
@@ -576,6 +642,21 @@ impl BackgroundRenderResources {
0,
bytemuck::cast_slice(&self.hand_dot_instances),
);
// Palm chips reuse the same live 12x7 pressure frame, but draw it as
// embedded micro-pixels inside dark chip tiles.
self.hand_palm_dot_instances = build_hand_palm_dot_instances(
self.rows,
self.cols,
self.hand_image_texture.width as f32,
self.hand_image_texture.height as f32,
pressure,
);
queue.write_buffer(
&self.hand_palm_dot_instance_buffer,
0,
bytemuck::cast_slice(&self.hand_palm_dot_instances),
);
}
fn paint(&self, render_pass: &mut wgpu::RenderPass<'_>, active_mode: &ActiveMode) {
@@ -623,6 +704,16 @@ impl BackgroundRenderResources {
render_pass.set_vertex_buffer(0, self.glyph_vertex_buffer.slice(..));
render_pass.set_vertex_buffer(1, self.hand_dot_instance_buffer.slice(..));
render_pass.draw(0..6, 0..self.hand_dot_instances.len() as u32);
render_pass.set_pipeline(&self.hand_palm_chip_pipeline);
render_pass.set_vertex_buffer(0, self.glyph_vertex_buffer.slice(..));
render_pass.set_vertex_buffer(1, self.hand_palm_chip_instance_buffer.slice(..));
render_pass.draw(0..6, 0..self.hand_palm_chip_instances.len() as u32);
render_pass.set_pipeline(&self.hand_palm_dot_pipeline);
render_pass.set_vertex_buffer(0, self.glyph_vertex_buffer.slice(..));
render_pass.set_vertex_buffer(1, self.hand_palm_dot_instance_buffer.slice(..));
render_pass.draw(0..6, 0..self.hand_palm_dot_instances.len() as u32);
}
fn visible_instance_count(&self, rows: u32, cols: u32) -> u32 {
@@ -956,6 +1047,72 @@ fn create_hand_dot_pipeline(
})
}
fn create_hand_palm_chip_pipeline(
device: &wgpu::Device,
target_format: &wgpu::TextureFormat,
shader: &wgpu::ShaderModule,
layout: &wgpu::PipelineLayout,
) -> wgpu::RenderPipeline {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Hand Palm Embedded Chip Pipeline"),
layout: Some(layout),
vertex: wgpu::VertexState {
module: shader,
entry_point: Some("vs_hand_palm_chip"),
compilation_options: Default::default(),
buffers: &[GlyphVertex::desc(), GlyphInstance::desc()],
},
fragment: Some(wgpu::FragmentState {
module: shader,
entry_point: Some("fs_hand_palm_chip"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format: *target_format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
})
}
fn create_hand_palm_dot_pipeline(
device: &wgpu::Device,
target_format: &wgpu::TextureFormat,
shader: &wgpu::ShaderModule,
layout: &wgpu::PipelineLayout,
) -> wgpu::RenderPipeline {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Hand Palm Embedded Chip Dot Pipeline"),
layout: Some(layout),
vertex: wgpu::VertexState {
module: shader,
entry_point: Some("vs_hand_palm_dot"),
compilation_options: Default::default(),
buffers: &[GlyphVertex::desc(), GlyphInstance::desc()],
},
fragment: Some(wgpu::FragmentState {
module: shader,
entry_point: Some("fs_hand_palm_dot"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format: *target_format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
})
}
fn build_hand_membrane_instances(image_width: f32, image_height: f32) -> Vec<GlyphInstance> {
HAND_TIP_MATRICES
.iter()
@@ -974,6 +1131,28 @@ fn build_hand_membrane_instances(image_width: f32, image_height: f32) -> Vec<Gly
.collect()
}
fn build_hand_palm_chip_instances(image_width: f32, image_height: f32) -> Vec<GlyphInstance> {
HAND_PALM_CHIPS
.iter()
.map(|chip| {
let uv_x = chip.center_px[0] / image_width.max(1.0);
let uv_y = chip.center_px[1] / image_height.max(1.0);
GlyphInstance {
// Palm chip shaders read xy as hand.png UV.
world_position: [uv_x, uv_y, 0.0, 1.0],
// Store chip angle, source-image pixel size, and matrix shape for the shader grid.
style: [
chip.angle_rad,
chip.size_px[0],
chip.size_px[1],
(chip.rows * 100 + chip.cols) as f32,
],
}
})
.collect()
}
fn build_hand_dot_instances(
rows: u32,
cols: u32,
@@ -1015,6 +1194,68 @@ fn build_hand_dot_instances(
instances
}
fn build_hand_palm_dot_instances(
rows: u32,
cols: u32,
image_width: f32,
image_height: f32,
pressure: &PressureFrame,
) -> Vec<GlyphInstance> {
let chip_dot_count: usize = HAND_PALM_CHIPS
.iter()
.map(|chip| (chip.rows * chip.cols) as usize)
.sum();
let mut instances = Vec::with_capacity(chip_dot_count);
for chip in HAND_PALM_CHIPS {
let cos = chip.angle_rad.cos();
let sin = chip.angle_rad.sin();
// Leave a bevel around the chip so the matrix reads as embedded pixels.
let active_size = [chip.size_px[0] * 0.72, chip.size_px[1] * 0.76];
for row in 0..chip.rows {
for col in 0..chip.cols {
// Current live data is still the device pressure frame. Sample it by
// proportion so the palm's 5x14 and 11x4 layouts get coherent values.
let source_row = resample_index(row, chip.rows, rows);
let source_col = resample_index(col, chip.cols, cols);
let index = (source_row * cols + source_col) as usize;
let [normalized, display_value] =
pressure.get(index).copied().unwrap_or([0.0, 0.0]);
let local_x =
(col as f32 - chip.cols as f32 / 2.0 + 0.5) / chip.cols as f32 * active_size[0];
let local_y =
(row as f32 - chip.rows as f32 / 2.0 + 0.5) / chip.rows as f32 * active_size[1];
let x = chip.center_px[0] + local_x * cos - local_y * sin;
let y = chip.center_px[1] + local_x * sin + local_y * cos;
instances.push(GlyphInstance {
world_position: [
x / image_width.max(1.0),
y / image_height.max(1.0),
0.0,
1.0,
],
style: [normalized, display_value, 0.0, 0.0],
});
}
}
}
instances
}
fn resample_index(index: u32, source_count: u32, target_count: u32) -> u32 {
if source_count <= 1 || target_count <= 1 {
return 0;
}
let mapped = index as f32 / (source_count - 1) as f32 * (target_count - 1) as f32;
mapped.round().clamp(0.0, (target_count - 1) as f32) as u32
}
fn build_glyph_instances(
rows: u32,
cols: u32,