feat: 添加 app/theme/ui/matrix/render 模块,重构 shader
This commit is contained in:
89
src/app.rs
Normal file
89
src/app.rs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
545
src/main.rs
545
src/main.rs
@@ -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<()> {
|
fn main() -> eframe::Result<()> {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
|
||||||
@@ -27,527 +20,3 @@ fn main() -> eframe::Result<()> {
|
|||||||
Box::new(|cc| Ok(Box::new(EskinDesktopApp::new(cc)))),
|
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
121
src/matrix.rs
Normal 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
333
src/render.rs
Normal 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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
203
src/shader.wgsl
203
src/shader.wgsl
@@ -1,52 +1,181 @@
|
|||||||
// 顶点着色器
|
struct MatrixUniform {
|
||||||
|
|
||||||
struct Camera {
|
|
||||||
view_proj: mat4x4f,
|
view_proj: mat4x4f,
|
||||||
}
|
viewport: vec4f,
|
||||||
@group(1) @binding(0)
|
glyph: vec4f,
|
||||||
var<uniform> camera: Camera;
|
color: vec4f,
|
||||||
|
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct VertexOutput {
|
@group(0) @binding(0)
|
||||||
|
var<uniform> u: MatrixUniform;
|
||||||
|
|
||||||
|
struct BackgroundVertexOutput {
|
||||||
@builtin(position) clip_position: vec4f,
|
@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
|
@vertex
|
||||||
fn vs_main(
|
fn vs_background(@builtin(vertex_index) vertex_index: u32) -> BackgroundVertexOutput {
|
||||||
model: VertexInput,
|
let positions = array<vec2f, 3>(
|
||||||
instance: InstanceInput,
|
vec2f(-1.0, -3.0),
|
||||||
) -> VertexOutput {
|
vec2f(3.0, 1.0),
|
||||||
let model_matrix = mat4x4f(
|
vec2f(-1.0, 1.0),
|
||||||
instance.model_matrix_0,
|
|
||||||
instance.model_matrix_1,
|
|
||||||
instance.model_matrix_2,
|
|
||||||
instance.model_matrix_3,
|
|
||||||
);
|
);
|
||||||
var out: VertexOutput;
|
|
||||||
out.tex_coords = model.tex_coords;
|
var out: BackgroundVertexOutput;
|
||||||
out.clip_position = camera.view_proj * model_matrix * vec4f(model.position, 1.0);
|
out.clip_position = vec4f(positions[vertex_index], 0.0, 1.0);
|
||||||
return out;
|
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)
|
let track_width = clamp(viewport.x * 0.42, 260.0, 560.0);
|
||||||
var t_diffuse: texture_2d<f32>;
|
let track_height = 12.0;
|
||||||
@group(0)@binding(1)
|
let track_center = vec2f(viewport.x * 0.5, viewport.y - 38.0);
|
||||||
var s_diffuse: sampler;
|
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
|
@fragment
|
||||||
fn fs_main(in: VertexOutput) -> @location(0) vec4f {
|
fn fs_glyph(in: GlyphVertexOutput) -> @location(0) vec4f {
|
||||||
return textureSample(t_diffuse, s_diffuse, in.tex_coords);
|
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
114
src/theme.rs
Normal 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
116
src/ui.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user