feat: 添加 app/theme/ui/matrix/render 模块,重构 shader

This commit is contained in:
lennlouisgeek
2026-05-18 02:17:46 +08:00
parent e0fbbd46a6
commit aff9c2a75c
7 changed files with 947 additions and 576 deletions

89
src/app.rs Normal file
View File

@@ -0,0 +1,89 @@
use std::time::Instant;
use eframe::{egui, egui_wgpu};
use crate::theme::{ENGINEERING_DARK, apply_theme};
use crate::{
matrix::{MATRIX_COLS, MATRIX_ROWS},
render::{BackgroundRenderResources, WgpuBackgroundCallback},
ui::{FloatingPanelState, draw_config_panel, draw_scene_panel, draw_stats_panel},
};
pub struct EskinDesktopApp {
scene_panel: FloatingPanelState,
config_panel: FloatingPanelState,
stats_panel: FloatingPanelState,
started_at: Instant,
}
impl EskinDesktopApp {
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
apply_theme(&cc.egui_ctx, &ENGINEERING_DARK);
let wgpu_state = cc
.wgpu_render_state
.as_ref()
.expect("need open eframe wgpu renderer feature");
let mut renderer = wgpu_state.renderer.write();
renderer
.callback_resources
.insert(BackgroundRenderResources::new(
&wgpu_state.device,
&wgpu_state.target_format,
MATRIX_ROWS,
MATRIX_COLS,
));
Self {
scene_panel: FloatingPanelState::new([16.0, 48.0], [16.0, 48.0]),
config_panel: FloatingPanelState::new([840.0, 48.0], [128.0, 48.0]),
stats_panel: FloatingPanelState::new([16.0, 520.0], [240.0, 48.0]),
started_at: Instant::now(),
}
}
fn draw_wgpu_background(&mut self, ui: &mut egui::Ui) {
let rect = ui.max_rect();
let width = rect.width().max(1.0);
let height = rect.height().max(1.0);
ui.painter().add(egui_wgpu::Callback::new_paint_callback(
rect,
WgpuBackgroundCallback {
width,
height,
time: self.started_at.elapsed().as_secs_f32(),
},
));
}
fn draw_toolbar(&mut self, ui: &mut egui::Ui) {
egui::Panel::top("main_menu").show_inside(ui, |ui| {
ui.horizontal(|ui| {
ui.checkbox(&mut self.scene_panel.visible, "Scene");
ui.checkbox(&mut self.config_panel.visible, "Config");
ui.checkbox(&mut self.stats_panel.visible, "Stats");
});
});
}
fn draw_floating_panels(&mut self, ctx: &egui::Context) {
draw_scene_panel(ctx, &mut self.scene_panel);
draw_config_panel(ctx, &mut self.config_panel);
draw_stats_panel(ctx, &mut self.stats_panel);
}
}
impl eframe::App for EskinDesktopApp {
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
let ctx = ui.ctx().clone();
self.draw_wgpu_background(ui);
self.draw_toolbar(ui);
self.draw_floating_panels(&ctx);
// Keep repainting while the wgpu background is a realtime viewport.
ctx.request_repaint();
}
}

View File

@@ -1,18 +1,11 @@
pub mod texture;
mod app;
mod matrix;
mod render;
mod theme;
mod ui;
use app::EskinDesktopApp;
use bytemuck;
use core::f32::consts;
use eframe::{
egui,
egui_wgpu::{self, wgpu},
wgpu::util::DeviceExt,
};
const NUM_INSTANCES_PER_ROW: u32 = 10;
const INSTANCE_DISPLACEMENT: glam::Vec3 = glam::Vec3::new(
NUM_INSTANCES_PER_ROW as f32 * 0.5,
0.0,
NUM_INSTANCES_PER_ROW as f32 * 0.5,
);
fn main() -> eframe::Result<()> {
env_logger::init();
@@ -27,527 +20,3 @@ fn main() -> eframe::Result<()> {
Box::new(|cc| Ok(Box::new(EskinDesktopApp::new(cc)))),
)
}
struct EskinDesktopApp {
show_scene_panel: bool,
show_config_panel: bool,
show_stats_panel: bool,
}
impl EskinDesktopApp {
fn new(cc: &eframe::CreationContext<'_>) -> Self {
let wgpu_state = cc
.wgpu_render_state
.as_ref()
.expect("need open eframe wgpu renderer feature");
let mut renderer = wgpu_state.renderer.write();
renderer
.callback_resources
.insert(BackgroundRenderResources::new(
&wgpu_state.device,
&wgpu_state.target_format,
&wgpu_state.queue,
12,
7,
));
Self {
show_scene_panel: true,
show_config_panel: true,
show_stats_panel: true,
}
}
fn draw_wgpu_background(&mut self, ui: &mut egui::Ui) {
let rect = ui.max_rect();
let width = rect.width();
let height = rect.height();
ui.painter().add(egui_wgpu::Callback::new_paint_callback(
rect,
WgpuBackgroundCallback {
aspect: width / height,
},
));
}
fn draw_toolbar(&mut self, ui: &mut egui::Ui) {
egui::Panel::top("main_menu").show_inside(ui, |ui| {
ui.horizontal(|ui| {
ui.checkbox(&mut self.show_scene_panel, "Scene");
ui.checkbox(&mut self.show_config_panel, "Config");
ui.checkbox(&mut self.show_stats_panel, "Stats");
});
});
}
fn draw_floating_panels(&mut self, ctx: &egui::Context) {
egui::Window::new("Scene")
.open(&mut self.show_scene_panel)
.default_pos([16.0, 48.0])
.show(ctx, |ui| {
ui.label("Models / materials / lights");
});
egui::Window::new("Config")
.open(&mut self.show_config_panel)
.default_pos([840.0, 48.0])
.show(ctx, |ui| {
ui.label("Render and viewport settings");
});
egui::Window::new("Stats")
.open(&mut self.show_stats_panel)
.default_pos([16.0, 520.0])
.show(ctx, |ui| {
ui.label("FPS / GPU info");
});
}
}
impl eframe::App for EskinDesktopApp {
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
let ctx = ui.ctx().clone();
self.draw_wgpu_background(ui);
self.draw_toolbar(ui);
self.draw_floating_panels(&ctx);
// Keep repainting while the wgpu background is a realtime viewport.
ctx.request_repaint();
}
}
struct WgpuBackgroundCallback {
aspect: f32,
}
impl egui_wgpu::CallbackTrait for WgpuBackgroundCallback {
fn prepare(
&self,
_device: &wgpu::Device,
queue: &wgpu::Queue,
_screen_descriptor: &egui_wgpu::ScreenDescriptor,
_egui_encoder: &mut wgpu::CommandEncoder,
resources: &mut egui_wgpu::CallbackResources,
) -> Vec<wgpu::CommandBuffer> {
let resources: &mut BackgroundRenderResources = resources.get_mut().unwrap();
resources.prepare(queue, self.aspect);
Vec::new()
}
fn paint(
&self,
_info: egui::PaintCallbackInfo,
render_pass: &mut wgpu::RenderPass<'static>,
resources: &egui_wgpu::CallbackResources,
) {
let resources: &BackgroundRenderResources = resources.get().unwrap();
resources.paint(render_pass);
}
}
struct BackgroundRenderResources {
camera: Camera,
camera_uniform: CameraUniform,
camera_buffer: wgpu::Buffer,
camera_bind_group: wgpu::BindGroup,
#[allow(dead_code)]
diffuse_texture: texture::Texture,
diffuse_bind_group: wgpu::BindGroup,
render_pipeline: wgpu::RenderPipeline,
vertex_buffer: wgpu::Buffer,
index_buffer: wgpu::Buffer,
num_indices: u32,
instance: Vec<Instance>,
instance_buffer: wgpu::Buffer,
cols: Option<u8>,
rows: Option<u8>,
}
impl BackgroundRenderResources {
fn new(
device: &wgpu::Device,
target_format: &wgpu::TextureFormat,
queue: &wgpu::Queue,
rows: u8,
cols: u8,
) -> Self {
let diffuse_bytes = include_bytes!("happy-tree.png");
let diffuse_texture =
texture::Texture::from_bytes(device, queue, diffuse_bytes, "happy-tree.png").unwrap();
let texture_group_bind_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("texture_bind_group_layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let diffuse_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("texture_bind_group"),
layout: &texture_group_bind_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&diffuse_texture.view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&diffuse_texture.sampler),
},
],
});
let camera = Camera {
eye: (0.0, 5.0, 10.0).into(),
target: (0.0, 0.0, 0.0).into(),
up: glam::Vec3::Y,
aspect: 1.0,
fovy: 45.0,
znear: 0.1,
zfar: 100.0,
};
let mut camera_uniform = CameraUniform::new();
camera_uniform.update_view_proj(&camera);
let camera_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Camera Buffer"),
contents: bytemuck::cast_slice(&[camera_uniform]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let instances = (0..NUM_INSTANCES_PER_ROW)
.flat_map(|z| {
(0..NUM_INSTANCES_PER_ROW).map(move |x| {
let position = glam::Vec3 {
x: x as f32,
y: 0.0,
z: z as f32,
} - INSTANCE_DISPLACEMENT;
let rotation = if position.length().abs() <= f32::EPSILON {
// this is needed so an object at (0, 0, 0) won't get scaled to zero
// as Quaternions can effect scale if they're not create correctly
glam::Quat::from_axis_angle(glam::Vec3::Z, 0.0)
} else {
glam::Quat::from_axis_angle(position.normalize(), consts::FRAC_PI_2)
};
Instance { position, rotation }
})
})
.collect::<Vec<_>>();
let instance_data = instances.iter().map(Instance::to_raw).collect::<Vec<_>>();
let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Instance Buffer"),
contents: bytemuck::cast_slice(&instance_data),
usage: wgpu::BufferUsages::VERTEX,
});
let camera_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("camera_bind_group_layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let camera_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("camera_bind_group"),
layout: &camera_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: camera_buffer.as_entire_binding(),
}],
});
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()),
});
let render_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Render Pipeline Layout"),
bind_group_layouts: &[
Some(&texture_group_bind_layout),
Some(&camera_bind_group_layout),
],
immediate_size: 0,
});
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Render Pipeline"),
layout: Some(&render_pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
compilation_options: Default::default(),
buffers: &[Vertex::desc(), InstanceRaw::desc()],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format: target_format.clone(),
blend: Some(wgpu::BlendState {
color: wgpu::BlendComponent::REPLACE,
alpha: wgpu::BlendComponent::REPLACE,
}),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: Some(wgpu::Face::Back),
polygon_mode: wgpu::PolygonMode::Fill,
// Requires Features::DEPTH_CLIP_CONTROL
unclipped_depth: false,
// Requires Features::CONSERVATIVE_RASTERIZATION
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview_mask: None,
cache: None,
});
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Vertex Buffer"),
contents: bytemuck::cast_slice(VERTICES),
usage: wgpu::BufferUsages::VERTEX,
});
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Index Buffer"),
contents: bytemuck::cast_slice(INDICES),
usage: wgpu::BufferUsages::INDEX,
});
let num_indices = INDICES.len() as u32;
Self {
camera,
camera_uniform,
camera_buffer,
camera_bind_group,
diffuse_texture,
diffuse_bind_group,
render_pipeline,
vertex_buffer,
index_buffer,
num_indices,
instance: instances,
instance_buffer,
cols: Some(cols),
rows: Some(rows),
}
}
// fn with_dot_matrix()
fn prepare(&mut self, queue: &wgpu::Queue, aspect: f32) {
self.camera.aspect = aspect;
self.camera_uniform.update_view_proj(&self.camera);
queue.write_buffer(
&self.camera_buffer,
0,
bytemuck::cast_slice(&[self.camera_uniform]),
);
}
fn paint(&self, render_pass: &mut wgpu::RenderPass<'_>) {
// TODO: set pipeline / bind groups / buffers and draw the model viewport here.
render_pass.set_pipeline(&self.render_pipeline);
render_pass.set_bind_group(0, &self.diffuse_bind_group, &[]);
render_pass.set_bind_group(1, &self.camera_bind_group, &[]);
render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
render_pass.set_vertex_buffer(1, self.instance_buffer.slice(..));
render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
// UPDATED!
render_pass.draw_indexed(0..self.num_indices, 0, 0..self.instance.len() as _);
}
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct Vertex {
position: [f32; 3],
tex_coords: [f32; 2],
}
impl Vertex {
fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
use core::mem;
wgpu::VertexBufferLayout {
array_stride: mem::size_of::<Vertex>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[
wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
shader_location: 1,
format: wgpu::VertexFormat::Float32x2,
},
],
}
}
}
const VERTICES: &[Vertex] = &[
Vertex {
position: [-0.0868241, 0.49240386, 0.0],
tex_coords: [0.4131759, 0.00759614],
}, // A
Vertex {
position: [-0.49513406, 0.06958647, 0.0],
tex_coords: [0.0048659444, 0.43041354],
}, // B
Vertex {
position: [-0.21918549, -0.44939706, 0.0],
tex_coords: [0.28081453, 0.949397],
}, // C
Vertex {
position: [0.35966998, -0.3473291, 0.0],
tex_coords: [0.85967, 0.84732914],
}, // D
Vertex {
position: [0.44147372, 0.2347359, 0.0],
tex_coords: [0.9414737, 0.2652641],
}, // E
];
const INDICES: &[u16] = &[0, 1, 4, 1, 2, 4, 2, 3, 4];
struct Camera {
eye: glam::Vec3,
target: glam::Vec3,
up: glam::Vec3,
aspect: f32,
fovy: f32,
znear: f32,
zfar: f32,
}
impl Camera {
fn build_view_projection_matrix(&self) -> glam::Mat4 {
let view = glam::Mat4::look_at_rh(self.eye, self.target, self.up);
let projection =
glam::Mat4::perspective_rh(self.fovy.to_radians(), self.aspect, self.znear, self.zfar);
projection * view
}
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct CameraUniform {
view_proj: [[f32; 4]; 4],
}
impl CameraUniform {
fn new() -> Self {
Self {
view_proj: glam::Mat4::IDENTITY.to_cols_array_2d(),
}
}
fn update_view_proj(&mut self, camera: &Camera) {
self.view_proj = camera.build_view_projection_matrix().to_cols_array_2d();
}
}
struct Instance {
position: glam::Vec3,
rotation: glam::Quat,
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct InstanceRaw {
model: [[f32; 4]; 4],
}
impl Instance {
fn to_raw(&self) -> InstanceRaw {
InstanceRaw {
model: (glam::Mat4::from_translation(self.position)
* glam::Mat4::from_quat(self.rotation))
.to_cols_array_2d(),
}
}
}
impl InstanceRaw {
fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
use core::mem;
wgpu::VertexBufferLayout {
array_stride: mem::size_of::<InstanceRaw>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute {
offset: 0,
shader_location: 5,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: core::mem::size_of::<[f32; 4]>() as wgpu::BufferAddress,
shader_location: 6,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: core::mem::size_of::<[f32; 8]>() as wgpu::BufferAddress,
shader_location: 7,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: core::mem::size_of::<[f32; 12]>() as wgpu::BufferAddress,
shader_location: 8,
format: wgpu::VertexFormat::Float32x4,
},
],
}
}
}

121
src/matrix.rs Normal file
View File

@@ -0,0 +1,121 @@
pub const MATRIX_ROWS: u32 = 12;
pub const MATRIX_COLS: u32 = 7;
const BASE_MATRIX_SPAN: f32 = 24.0;
const MATRIX_SPAN_GROWTH: f32 = 0.6;
const MIN_MATRIX_SPAN: f32 = 24.0;
const MAX_MATRIX_SPAN: f32 = 58.0;
const MIN_CELL_SPACING: f32 = 0.52;
const MAX_CELL_SPACING: f32 = 3.8;
const MIN_BOARD_PADDING: f32 = 2.6;
const MAX_BOARD_PADDING: f32 = 6.8;
const MATRIX_OFFSET_Y: f32 = -2.4;
const MATRIX_OFFSET_Z: f32 = 12.0;
const HEIGHT_SCALE: f32 = 10.6;
const BASE_HEIGHT: f32 = 0.12;
const CAMERA_FOV: f32 = 36.0;
const CAMERA_DISTANCE_MIN: f32 = 30.0;
const CAMERA_DISTANCE_MAX: f32 = 122.0;
const CAMERA_FIT_PADDING: f32 = 1.04;
const CAMERA_ELEVATION_DEG: f32 = 64.0;
const CAMERA_TARGET_X: f32 = 0.0;
const CAMERA_TARGET_Y: f32 = MATRIX_OFFSET_Y + 0.2;
const CAMERA_TARGET_Z: f32 = MATRIX_OFFSET_Z - 0.4;
pub struct MatrixLayout {
pub cell_spacing: f32,
pub board_width: f32,
pub board_depth: f32,
pub board_padding: f32,
pub label_float_offset: f32,
}
impl MatrixLayout {
pub fn new(rows: u32, cols: u32) -> Self {
let longest_edge = rows.max(cols).max(1) as f32;
let edge_span = (longest_edge - 1.0).max(1.0);
let target_span = (BASE_MATRIX_SPAN + edge_span * MATRIX_SPAN_GROWTH)
.clamp(MIN_MATRIX_SPAN, MAX_MATRIX_SPAN);
let cell_spacing = (target_span / edge_span).clamp(MIN_CELL_SPACING, MAX_CELL_SPACING);
let board_width = cols.max(1) as f32 * cell_spacing;
let board_depth = rows.max(1) as f32 * cell_spacing;
let board_padding = (cell_spacing * 1.62).clamp(MIN_BOARD_PADDING, MAX_BOARD_PADDING);
let label_float_offset = (cell_spacing * 0.42).clamp(0.36, 1.12);
Self {
cell_spacing,
board_width,
board_depth,
board_padding,
label_float_offset,
}
}
}
pub fn build_view_projection(aspect: f32, layout: &MatrixLayout) -> [[f32; 4]; 4] {
let camera_distance = fit_camera_distance(
layout.board_width,
layout.board_depth,
layout.board_padding,
aspect,
);
let elevation = CAMERA_ELEVATION_DEG.to_radians();
let target = glam::Vec3::new(CAMERA_TARGET_X, CAMERA_TARGET_Y, CAMERA_TARGET_Z);
let eye = glam::Vec3::new(
CAMERA_TARGET_X,
CAMERA_TARGET_Y + elevation.sin() * camera_distance,
CAMERA_TARGET_Z + elevation.cos() * camera_distance,
);
let view = glam::Mat4::look_at_rh(eye, target, glam::Vec3::Y);
let projection = glam::Mat4::perspective_rh(CAMERA_FOV.to_radians(), aspect, 0.1, 500.0);
let open_gl_to_wgpu = glam::Mat4::from_cols_array(&[
1.0, 0.0, 0.0, 0.0, //
0.0, 1.0, 0.0, 0.0, //
0.0, 0.0, 0.5, 0.0, //
0.0, 0.0, 0.5, 1.0,
]);
(open_gl_to_wgpu * projection * view).to_cols_array_2d()
}
pub fn glyph_world_position(
row: u32,
col: u32,
rows: u32,
cols: u32,
layout: &MatrixLayout,
time: f32,
) -> ([f32; 4], f32) {
let normalized = demo_pressure(row, col, time);
let height = BASE_HEIGHT + normalized.powf(0.9) * HEIGHT_SCALE;
let x = (col as f32 - cols as f32 / 2.0 + 0.5) * layout.cell_spacing;
let z = (row as f32 - rows as f32 / 2.0 + 0.5) * layout.cell_spacing;
(
[
x,
MATRIX_OFFSET_Y + height + layout.label_float_offset,
MATRIX_OFFSET_Z + z,
1.0,
],
normalized,
)
}
fn demo_pressure(row: u32, col: u32, time: f32) -> f32 {
let seed = ((row * 17 + col * 31) as f32).sin() * 43_758.547;
let phase = seed.fract() * std::f32::consts::TAU;
let slow = 0.5 + 0.5 * (time * 1.35 + phase).sin();
let row_wave = 0.5 + 0.5 * (time * 0.82 + row as f32 * 0.54 + col as f32 * 0.19).sin();
(slow * row_wave).powf(0.72).clamp(0.0, 1.0)
}
fn fit_camera_distance(board_width: f32, board_depth: f32, board_padding: f32, aspect: f32) -> f32 {
let padded_width = board_width + board_padding * 2.0;
let padded_depth = board_depth + board_padding * 2.0;
let safe_aspect = aspect.max(0.5);
let effective_half_span = (padded_depth * 0.5).max((padded_width * 0.5) / safe_aspect);
let fov_radians = (CAMERA_FOV * 0.5).to_radians();
let fit_distance = (effective_half_span / fov_radians.tan()) * CAMERA_FIT_PADDING;
fit_distance.clamp(CAMERA_DISTANCE_MIN, CAMERA_DISTANCE_MAX)
}

333
src/render.rs Normal file
View File

@@ -0,0 +1,333 @@
use eframe::{
egui,
egui_wgpu::{self, wgpu},
wgpu::util::DeviceExt,
};
use crate::matrix::{MatrixLayout, build_view_projection, glyph_world_position};
pub struct WgpuBackgroundCallback {
pub width: f32,
pub height: f32,
pub time: f32,
}
impl egui_wgpu::CallbackTrait for WgpuBackgroundCallback {
fn prepare(
&self,
_device: &wgpu::Device,
queue: &wgpu::Queue,
_screen_descriptor: &egui_wgpu::ScreenDescriptor,
_egui_encoder: &mut wgpu::CommandEncoder,
resources: &mut egui_wgpu::CallbackResources,
) -> Vec<wgpu::CommandBuffer> {
let resources: &mut BackgroundRenderResources = resources.get_mut().unwrap();
resources.prepare(queue, self.width, self.height, self.time);
Vec::new()
}
fn paint(
&self,
_info: egui::PaintCallbackInfo,
render_pass: &mut wgpu::RenderPass<'static>,
resources: &egui_wgpu::CallbackResources,
) {
let resources: &BackgroundRenderResources = resources.get().unwrap();
resources.paint(render_pass);
}
}
pub struct BackgroundRenderResources {
layout: MatrixLayout,
rows: u32,
cols: u32,
uniform: MatrixUniform,
uniform_buffer: wgpu::Buffer,
uniform_bind_group: wgpu::BindGroup,
background_pipeline: wgpu::RenderPipeline,
glyph_pipeline: wgpu::RenderPipeline,
glyph_vertex_buffer: wgpu::Buffer,
glyph_instance_buffer: wgpu::Buffer,
glyph_instances: Vec<GlyphInstance>,
}
impl BackgroundRenderResources {
pub fn new(
device: &wgpu::Device,
target_format: &wgpu::TextureFormat,
rows: u32,
cols: u32,
) -> Self {
let layout = MatrixLayout::new(rows, cols);
let uniform = MatrixUniform::new(1.0, 1.0, build_view_projection(1.0, &layout));
let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Pressure Matrix Uniform Buffer"),
contents: bytemuck::cast_slice(&[uniform]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let uniform_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("pressure_matrix_bind_group_layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("pressure_matrix_bind_group"),
layout: &uniform_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
});
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Pressure Matrix Shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()),
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Pressure Matrix Pipeline Layout"),
bind_group_layouts: &[Some(&uniform_bind_group_layout)],
immediate_size: 0,
});
let background_pipeline =
create_background_pipeline(device, target_format, &shader, &pipeline_layout);
let glyph_pipeline =
create_glyph_pipeline(device, target_format, &shader, &pipeline_layout);
let glyph_vertices = [
GlyphVertex {
local: [-1.0, -1.0],
},
GlyphVertex { local: [1.0, -1.0] },
GlyphVertex { local: [-1.0, 1.0] },
GlyphVertex { local: [-1.0, 1.0] },
GlyphVertex { local: [1.0, -1.0] },
GlyphVertex { local: [1.0, 1.0] },
];
let glyph_vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Pressure Glyph Vertex Buffer"),
contents: bytemuck::cast_slice(&glyph_vertices),
usage: wgpu::BufferUsages::VERTEX,
});
let glyph_instances = build_glyph_instances(rows, cols, &layout, 0.0);
let glyph_instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Pressure Glyph Instance Buffer"),
contents: bytemuck::cast_slice(&glyph_instances),
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
});
Self {
layout,
rows,
cols,
uniform,
uniform_buffer,
uniform_bind_group,
background_pipeline,
glyph_pipeline,
glyph_vertex_buffer,
glyph_instance_buffer,
glyph_instances,
}
}
fn prepare(&mut self, queue: &wgpu::Queue, width: f32, height: f32, time: f32) {
let aspect = width / height.max(1.0);
self.uniform =
MatrixUniform::new(width, height, build_view_projection(aspect, &self.layout));
queue.write_buffer(
&self.uniform_buffer,
0,
bytemuck::cast_slice(&[self.uniform]),
);
self.glyph_instances = build_glyph_instances(self.rows, self.cols, &self.layout, time);
queue.write_buffer(
&self.glyph_instance_buffer,
0,
bytemuck::cast_slice(&self.glyph_instances),
);
}
fn paint(&self, render_pass: &mut wgpu::RenderPass<'_>) {
render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
render_pass.set_pipeline(&self.background_pipeline);
render_pass.draw(0..3, 0..1);
render_pass.set_pipeline(&self.glyph_pipeline);
render_pass.set_vertex_buffer(0, self.glyph_vertex_buffer.slice(..));
render_pass.set_vertex_buffer(1, self.glyph_instance_buffer.slice(..));
render_pass.draw(0..6, 0..self.glyph_instances.len() as u32);
}
}
fn create_background_pipeline(
device: &wgpu::Device,
target_format: &wgpu::TextureFormat,
shader: &wgpu::ShaderModule,
layout: &wgpu::PipelineLayout,
) -> wgpu::RenderPipeline {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Pressure Matrix Background Pipeline"),
layout: Some(layout),
vertex: wgpu::VertexState {
module: shader,
entry_point: Some("vs_background"),
compilation_options: Default::default(),
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: shader,
entry_point: Some("fs_background"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format: *target_format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
})
}
fn create_glyph_pipeline(
device: &wgpu::Device,
target_format: &wgpu::TextureFormat,
shader: &wgpu::ShaderModule,
layout: &wgpu::PipelineLayout,
) -> wgpu::RenderPipeline {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Pressure Matrix Glyph Pipeline"),
layout: Some(layout),
vertex: wgpu::VertexState {
module: shader,
entry_point: Some("vs_glyph"),
compilation_options: Default::default(),
buffers: &[GlyphVertex::desc(), GlyphInstance::desc()],
},
fragment: Some(wgpu::FragmentState {
module: shader,
entry_point: Some("fs_glyph"),
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_glyph_instances(
rows: u32,
cols: u32,
layout: &MatrixLayout,
time: f32,
) -> Vec<GlyphInstance> {
let mut instances = Vec::with_capacity((rows * cols) as usize);
for row in 0..rows {
for col in 0..cols {
let (world_position, normalized) =
glyph_world_position(row, col, rows, cols, layout, time);
instances.push(GlyphInstance {
world_position,
style: [normalized, 0.0, 0.0, 0.0],
});
}
}
instances
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct MatrixUniform {
view_proj: [[f32; 4]; 4],
viewport: [f32; 4],
glyph: [f32; 4],
color: [f32; 4],
}
impl MatrixUniform {
fn new(width: f32, height: f32, view_proj: [[f32; 4]; 4]) -> Self {
Self {
view_proj,
viewport: [width.max(1.0), height.max(1.0), 0.0, 0.0],
glyph: [16.0, 0.0, 0.0, 0.0],
color: [0.05, 0.92, 0.32, 1.0],
}
}
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct GlyphVertex {
local: [f32; 2],
}
impl GlyphVertex {
fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<GlyphVertex>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x2,
}],
}
}
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct GlyphInstance {
world_position: [f32; 4],
style: [f32; 4],
}
impl GlyphInstance {
fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<GlyphInstance>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute {
offset: 0,
shader_location: 1,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: std::mem::size_of::<[f32; 4]>() as wgpu::BufferAddress,
shader_location: 2,
format: wgpu::VertexFormat::Float32x4,
},
],
}
}
}

View File

@@ -1,52 +1,181 @@
// 顶点着色器
struct Camera {
struct MatrixUniform {
view_proj: mat4x4f,
}
@group(1) @binding(0)
var<uniform> camera: Camera;
struct VertexInput {
@location(0) position: vec3f,
@location(1) tex_coords: vec2f,
}
struct InstanceInput {
@location(5) model_matrix_0: vec4f,
@location(6) model_matrix_1: vec4f,
@location(7) model_matrix_2: vec4f,
@location(8) model_matrix_3: vec4f,
viewport: vec4f,
glyph: vec4f,
color: vec4f,
}
struct VertexOutput {
@group(0) @binding(0)
var<uniform> u: MatrixUniform;
struct BackgroundVertexOutput {
@builtin(position) clip_position: vec4f,
@location(0) tex_coords: vec2f,
}
struct GlyphVertexInput {
@location(0) local: vec2f,
}
struct GlyphInstanceInput {
@location(1) world_position: vec4f,
@location(2) style: vec4f,
}
struct GlyphVertexOutput {
@builtin(position) clip_position: vec4f,
@location(0) local: vec2f,
@location(1) intensity: f32,
}
fn saturate(value: f32) -> f32 {
return clamp(value, 0.0, 1.0);
}
fn capsule_distance(point: vec2f, start: vec2f, end: vec2f) -> f32 {
let segment = end - start;
let h = saturate(dot(point - start, segment) / dot(segment, segment));
return length(point - start - segment * h);
}
fn rotate2(point: vec2f, angle: f32) -> vec2f {
let s = sin(angle);
let c = cos(angle);
return vec2f(point.x * c - point.y * s, point.x * s + point.y * c);
}
fn range_stop_color(index: u32) -> vec3f {
switch index {
case 0u: {
return vec3f(0.263, 0.667, 0.420);
}
case 1u: {
return vec3f(0.094, 0.735, 0.875);
}
case 2u: {
return vec3f(0.298, 0.349, 0.957);
}
case 3u: {
return vec3f(0.812, 0.294, 0.851);
}
case 4u: {
return vec3f(0.957, 0.337, 0.333);
}
default: {
return vec3f(1.000, 0.596, 0.204);
}
}
}
fn sample_range_color(value: f32) -> vec3f {
let t = saturate(value);
if (t <= 0.25) {
let local = smoothstep(0.0, 0.25, t);
return mix(range_stop_color(0u), range_stop_color(1u), local);
}
if (t <= 0.5) {
let local = smoothstep(0.25, 0.5, t);
return mix(range_stop_color(1u), range_stop_color(2u), local);
}
if (t <= 0.75) {
let local = smoothstep(0.5, 0.75, t);
return mix(range_stop_color(2u), range_stop_color(3u), local);
}
if (t <= 0.875) {
let local = smoothstep(0.75, 0.875, t);
return mix(range_stop_color(3u), range_stop_color(4u), local);
}
let local = smoothstep(0.875, 1.0, t);
return mix(range_stop_color(4u), range_stop_color(5u), local);
}
fn digit_zero_alpha(local: vec2f) -> f32 {
let tilted = rotate2(local, -0.18);
let oval = vec2f(tilted.x / 0.48, tilted.y / 0.75);
let aa = 0.030;
let outer = 1.0 - smoothstep(0.93 - aa, 0.93 + aa, length(oval));
let inner = 1.0 - smoothstep(0.57 - aa, 0.57 + aa, length(oval));
let ring = saturate(outer - inner);
let slash = 1.0 - smoothstep(
0.036,
0.066,
capsule_distance(tilted, vec2f(-0.16, 0.43), vec2f(0.18, -0.43)),
);
return max(ring, slash * 0.88);
}
@vertex
fn vs_main(
model: VertexInput,
instance: InstanceInput,
) -> VertexOutput {
let model_matrix = mat4x4f(
instance.model_matrix_0,
instance.model_matrix_1,
instance.model_matrix_2,
instance.model_matrix_3,
fn vs_background(@builtin(vertex_index) vertex_index: u32) -> BackgroundVertexOutput {
let positions = array<vec2f, 3>(
vec2f(-1.0, -3.0),
vec2f(3.0, 1.0),
vec2f(-1.0, 1.0),
);
var out: VertexOutput;
out.tex_coords = model.tex_coords;
out.clip_position = camera.view_proj * model_matrix * vec4f(model.position, 1.0);
var out: BackgroundVertexOutput;
out.clip_position = vec4f(positions[vertex_index], 0.0, 1.0);
return out;
}
// 片元着色器
@fragment
fn fs_background(@builtin(position) frag_coord: vec4f) -> @location(0) vec4f {
let pixel = frag_coord.xy;
let viewport = u.viewport.xy;
let uv = pixel / max(viewport, vec2f(1.0, 1.0));
var color = mix(vec3f(0.112, 0.126, 0.160), vec3f(0.145, 0.160, 0.198), 1.0 - uv.y);
let vignette = smoothstep(0.18, 0.92, length((uv - vec2f(0.52, 0.48)) * vec2f(viewport.x / viewport.y, 1.0)));
color *= 1.0 - vignette * 0.30;
color += vec3f(0.035, 0.070, 0.090) * (1.0 - smoothstep(0.0, 0.9, abs(uv.y - 0.52)));
@group(0) @binding(0)
var t_diffuse: texture_2d<f32>;
@group(0)@binding(1)
var s_diffuse: sampler;
let track_width = clamp(viewport.x * 0.42, 260.0, 560.0);
let track_height = 12.0;
let track_center = vec2f(viewport.x * 0.5, viewport.y - 38.0);
let local = pixel - track_center;
let half_size = vec2f(track_width * 0.5, track_height * 0.5);
let inside_track = step(abs(local.x), half_size.x) * step(abs(local.y), half_size.y);
let border = step(abs(local.x), half_size.x + 1.0) * step(abs(local.y), half_size.y + 1.0) - inside_track;
let t = saturate((local.x + half_size.x) / track_width);
if (inside_track > 0.5) {
let shade = mix(0.72, 1.0, 1.0 - saturate((local.y + half_size.y) / track_height));
color = mix(color, sample_range_color(t) * shade, 0.96);
}
color = max(color, vec3f(0.34, 0.41, 0.46) * border);
let tick_area = step(abs(local.x), half_size.x) * step(abs(local.y), half_size.y + 8.0);
if (tick_area > 0.5) {
let ticks = array<f32, 6>(0.0, 0.25, 0.5, 0.75, 0.875, 1.0);
for (var index = 0u; index < 6u; index = index + 1u) {
let tick_x = (ticks[index] - 0.5) * track_width;
let tick = step(abs(local.x - tick_x), 1.0) * step(abs(local.y), half_size.y + 7.0);
color = max(color, vec3f(0.78, 0.84, 0.90) * tick);
}
}
return vec4f(color, 1.0);
}
@vertex
fn vs_glyph(vertex: GlyphVertexInput, instance: GlyphInstanceInput) -> GlyphVertexOutput {
let center = u.view_proj * vec4f(instance.world_position.xyz, 1.0);
let pixel_size = u.glyph.x * mix(0.92, 1.10, saturate(instance.style.x));
let ndc_offset = vertex.local * vec2f(pixel_size / u.viewport.x, pixel_size / u.viewport.y) * 2.0;
var out: GlyphVertexOutput;
out.clip_position = vec4f(center.xy + ndc_offset * center.w, center.z, center.w);
out.local = vertex.local;
out.intensity = instance.style.x;
return out;
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4f {
return textureSample(t_diffuse, s_diffuse, in.tex_coords);
}
fn fs_glyph(in: GlyphVertexOutput) -> @location(0) vec4f {
let alpha = digit_zero_alpha(in.local);
let color = sample_range_color(in.intensity) * 1.08;
return vec4f(color, alpha);
}

114
src/theme.rs Normal file
View File

@@ -0,0 +1,114 @@
use eframe::egui;
#[derive(Clone, Copy)]
pub struct AppTheme {
pub bg: egui::Color32,
pub panel: egui::Color32,
pub panel_strong: egui::Color32,
pub panel_deep: egui::Color32,
pub border: egui::Color32,
pub border_soft: egui::Color32,
pub text: egui::Color32,
pub text_dim: egui::Color32,
pub accent: egui::Color32,
pub accent_hot: egui::Color32,
pub radius: u8,
}
pub const ENGINEERING_DARK: AppTheme = AppTheme {
bg: egui::Color32::from_rgb(27, 34, 44),
panel: egui::Color32::from_rgb(29, 40, 52),
panel_strong: egui::Color32::from_rgb(39, 51, 65),
panel_deep: egui::Color32::from_rgb(15, 21, 29),
border: egui::Color32::from_rgb(70, 87, 104),
border_soft: egui::Color32::from_rgb(48, 63, 78),
text: egui::Color32::from_rgb(224, 231, 238),
text_dim: egui::Color32::from_rgb(151, 166, 184),
accent: egui::Color32::from_rgb(255, 121, 47),
accent_hot: egui::Color32::from_rgb(255, 163, 82),
radius: 3,
};
pub fn apply_theme(ctx: &egui::Context, theme: &AppTheme) {
let mut visuals = egui::Visuals::dark();
visuals.override_text_color = Some(theme.text);
visuals.panel_fill = theme.bg;
visuals.window_fill = theme.panel;
visuals.window_stroke = egui::Stroke::new(1.0, theme.border);
visuals.extreme_bg_color = theme.panel_deep;
visuals.faint_bg_color = theme.panel_strong;
visuals.code_bg_color = theme.panel_deep;
visuals.warn_fg_color = theme.accent_hot;
visuals.error_fg_color = egui::Color32::from_rgb(255, 98, 82);
visuals.widgets.noninteractive.bg_fill = theme.panel_strong;
visuals.widgets.noninteractive.bg_stroke = egui::Stroke::new(1.0, theme.border_soft);
visuals.widgets.noninteractive.fg_stroke = egui::Stroke::new(1.0, theme.text_dim);
visuals.widgets.inactive.bg_fill = theme.panel_strong;
visuals.widgets.inactive.bg_stroke = egui::Stroke::new(1.0, theme.border_soft);
visuals.widgets.inactive.fg_stroke = egui::Stroke::new(1.0, theme.text);
visuals.widgets.hovered.bg_fill = egui::Color32::from_rgb(51, 66, 82);
visuals.widgets.hovered.bg_stroke = egui::Stroke::new(1.0, theme.border);
visuals.widgets.hovered.fg_stroke = egui::Stroke::new(1.0, theme.text);
visuals.widgets.active.bg_fill = theme.accent;
visuals.widgets.active.bg_stroke = egui::Stroke::new(1.0, theme.accent_hot);
visuals.widgets.active.fg_stroke = egui::Stroke::new(1.0, egui::Color32::WHITE);
visuals.widgets.open.bg_fill = egui::Color32::from_rgb(45, 58, 72);
visuals.widgets.open.bg_stroke = egui::Stroke::new(1.0, theme.border);
visuals.widgets.open.fg_stroke = egui::Stroke::new(1.0, theme.text);
visuals.selection.bg_fill = theme.accent;
visuals.selection.stroke = egui::Stroke::new(1.0, egui::Color32::WHITE);
visuals.hyperlink_color = theme.accent_hot;
ctx.set_visuals(visuals);
let mut style = (*ctx.global_style()).clone();
style.spacing.item_spacing = egui::vec2(7.0, 5.0);
style.spacing.window_margin = egui::Margin::same(7);
style.visuals.window_corner_radius = egui::CornerRadius::same(theme.radius);
style.visuals.menu_corner_radius = egui::CornerRadius::same(2);
style.visuals.window_shadow = egui::epaint::Shadow {
offset: [0, 12],
blur: 24,
spread: 0,
color: egui::Color32::from_black_alpha(110),
};
ctx.set_global_style(style);
}
pub fn panel_frame(ctx: &egui::Context) -> egui::Frame {
let style = ctx.global_style();
egui::Frame::window(&style)
.fill(ENGINEERING_DARK.panel)
.stroke(egui::Stroke::new(1.0, ENGINEERING_DARK.border))
.corner_radius(egui::CornerRadius::same(ENGINEERING_DARK.radius))
.shadow(egui::epaint::Shadow {
offset: [0, 12],
blur: 26,
spread: 0,
color: egui::Color32::from_black_alpha(120),
})
}
pub fn group_frame() -> egui::Frame {
egui::Frame::new()
.fill(ENGINEERING_DARK.panel_deep)
.stroke(egui::Stroke::new(1.0, ENGINEERING_DARK.border_soft))
.corner_radius(egui::CornerRadius::same(2))
.inner_margin(egui::Margin::symmetric(6, 5))
}
pub fn tag_button(label: impl Into<egui::WidgetText>) -> egui::Button<'static> {
egui::Button::new(label)
.fill(ENGINEERING_DARK.panel_strong)
.stroke(egui::Stroke::new(1.0, ENGINEERING_DARK.border))
.corner_radius(egui::CornerRadius::same(2))
}
pub fn dim_text() -> egui::Color32 {
ENGINEERING_DARK.text_dim
}
pub fn accent_text() -> egui::Color32 {
ENGINEERING_DARK.accent_hot
}

116
src/ui.rs Normal file
View File

@@ -0,0 +1,116 @@
use eframe::egui;
use crate::theme::{accent_text, dim_text, group_frame, panel_frame, tag_button};
pub struct FloatingPanelState {
pub visible: bool,
default_pos: egui::Pos2,
tag_pos: egui::Pos2,
}
impl FloatingPanelState {
pub fn new(default_pos: [f32; 2], tag_pos: [f32; 2]) -> Self {
Self {
visible: true,
default_pos: egui::pos2(default_pos[0], default_pos[1]),
tag_pos: egui::pos2(tag_pos[0], tag_pos[1]),
}
}
}
pub fn draw_scene_panel(ctx: &egui::Context, panel: &mut FloatingPanelState) {
draw_floating_panel(ctx, panel, "Scene", "scene_panel", |ui| {
ui.horizontal(|ui| {
ui.colored_label(dim_text(), "View");
let _ = ui.selectable_label(true, "Clusters");
let _ = ui.selectable_label(false, "Triangles");
});
ui.separator();
group_frame().show(ui, |ui| {
ui.label("Models / materials / lights");
ui.label("target tasks 64");
ui.label("slack cache 100.0%");
});
});
}
pub fn draw_config_panel(ctx: &egui::Context, panel: &mut FloatingPanelState) {
draw_floating_panel(ctx, panel, "Config", "config_panel", |ui| {
ui.horizontal(|ui| {
ui.label("Shader");
let _ = ui.selectable_label(true, "Clusters");
let _ = ui.selectable_label(false, "wireframe");
});
ui.horizontal(|ui| {
ui.label("Grid");
ui.label("x 32");
ui.label("z 32");
ui.label("side 1.000");
});
});
}
pub fn draw_stats_panel(ctx: &egui::Context, panel: &mut FloatingPanelState) {
draw_floating_panel(ctx, panel, "Stats", "stats_panel", |ui| {
ui.horizontal(|ui| {
ui.colored_label(accent_text(), "0.030");
ui.label("81m:51s");
});
ui.separator();
group_frame().show(ui, |ui| {
ui.label("FPS / GPU info");
ui.label("bounds 589.0us");
ui.label("clusters 12.8ms");
});
});
}
fn draw_floating_panel(
ctx: &egui::Context,
panel: &mut FloatingPanelState,
title: &'static str,
id: &'static str,
add_contents: impl FnOnce(&mut egui::Ui),
) {
if panel.visible {
let mut open = true;
let mut hide_requested = false;
egui::Window::new(title)
.id(egui::Id::new(id))
.open(&mut open)
.default_pos(panel.default_pos)
.title_bar(false)
.resizable(true)
.frame(panel_frame(ctx))
.show(ctx, |ui| {
ui.horizontal(|ui| {
if ui.add(tag_button("Hide")).clicked() {
hide_requested = true;
}
ui.add_space(6.0);
ui.colored_label(dim_text(), title);
});
ui.separator();
add_contents(ui);
});
panel.visible = open && !hide_requested;
} else {
let response = egui::Area::new(egui::Id::new(format!("{id}_tag")))
.current_pos(panel.tag_pos)
.movable(true)
.order(egui::Order::Foreground)
.show(ctx, |ui| {
ui.set_min_width(86.0);
if ui
.add(tag_button(format!("{title}")).min_size(egui::vec2(86.0, 22.0)))
.clicked()
{
panel.visible = true;
}
});
panel.tag_pos = response.response.rect.min;
}
}