Compare commits
2 Commits
2497cb93ff
...
156df50d33
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
156df50d33 | ||
|
|
ec46e53c75 |
17
Cargo.lock
generated
17
Cargo.lock
generated
@@ -1301,10 +1301,12 @@ dependencies = [
|
|||||||
"eframe",
|
"eframe",
|
||||||
"egui_extras",
|
"egui_extras",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
"fs_extra",
|
||||||
"glam",
|
"glam",
|
||||||
"image",
|
"image",
|
||||||
"log",
|
"log",
|
||||||
"serialport",
|
"serialport",
|
||||||
|
"tobj",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1455,6 +1457,12 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fs_extra"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "futures-core"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
@@ -3908,6 +3916,15 @@ dependencies = [
|
|||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tobj"
|
||||||
|
version = "4.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2da83817ac7401436633efc35c19edae9f326fa741fae775a1caa406de9db07c"
|
||||||
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "1.1.1+spec-1.1.0"
|
version = "1.1.1+spec-1.1.0"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
name = "eskin-model-player"
|
name = "eskin-model-player"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
build = "build.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
eframe = { version = "0.34.2", features = ["default", "wgpu", "__screenshot"] }
|
eframe = { version = "0.34.2", features = ["default", "wgpu", "__screenshot"] }
|
||||||
@@ -15,3 +16,8 @@ egui_extras = { version = "0.34.2", features = ["image"] }
|
|||||||
crossbeam-channel = "0.5.15"
|
crossbeam-channel = "0.5.15"
|
||||||
crc = "3.4.0"
|
crc = "3.4.0"
|
||||||
log = "0.4.29"
|
log = "0.4.29"
|
||||||
|
tobj = "4.0.4"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
anyhow = "1.0.102"
|
||||||
|
fs_extra = "1.3.0"
|
||||||
|
|||||||
33
build.rs
Normal file
33
build.rs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
use std::{env, fs, path::PathBuf};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use fs_extra::{copy_items, dir::CopyOptions};
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?);
|
||||||
|
let source_dir = manifest_dir.join("res");
|
||||||
|
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
|
||||||
|
let resource_dir = out_dir.join("res");
|
||||||
|
|
||||||
|
let mut copy_options = CopyOptions::new();
|
||||||
|
copy_options.overwrite = true;
|
||||||
|
|
||||||
|
println!("cargo:rerun-if-changed={}", source_dir.display());
|
||||||
|
|
||||||
|
if resource_dir.exists() {
|
||||||
|
fs::remove_dir_all(&resource_dir)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if source_dir.exists() {
|
||||||
|
let items = fs::read_dir(&source_dir)?
|
||||||
|
.map(|entry| entry.map(|entry| entry.path()))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
|
fs::create_dir_all(&resource_dir)?;
|
||||||
|
copy_items(&items, &resource_dir, ©_options)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("cargo:rustc-env=RESOURCE_DIR={}", resource_dir.display());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
BIN
res/cube-diffuse.jpg
Normal file
BIN
res/cube-diffuse.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
res/cube-normal.png
Normal file
BIN
res/cube-normal.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 117 KiB |
14
res/cube.mtl
Normal file
14
res/cube.mtl
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Blender MTL File: 'cube.blend'
|
||||||
|
# Material Count: 1
|
||||||
|
|
||||||
|
newmtl Material.001
|
||||||
|
Ns 323.999994
|
||||||
|
Ka 1.000000 1.000000 1.000000
|
||||||
|
Kd 0.800000 0.800000 0.800000
|
||||||
|
Ks 0.500000 0.500000 0.500000
|
||||||
|
Ke 0.000000 0.000000 0.000000
|
||||||
|
Ni 1.450000
|
||||||
|
d 1.000000
|
||||||
|
illum 2
|
||||||
|
map_Bump cube-normal.png
|
||||||
|
map_Kd cube-diffuse.jpg
|
||||||
1143
res/cube.obj
Normal file
1143
res/cube.obj
Normal file
File diff suppressed because it is too large
Load Diff
17
src/app.rs
17
src/app.rs
@@ -1,13 +1,11 @@
|
|||||||
use eframe::{egui, egui_wgpu};
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use crate::connection::ConnectionManager;
|
use crate::connection::ConnectionManager;
|
||||||
use crate::recording::Recorder;
|
use crate::recording::Recorder;
|
||||||
use crate::theme::{ONE_DARK_PRO, apply_fonts, apply_theme};
|
use crate::theme::{ONE_DARK_PRO, apply_fonts, apply_theme};
|
||||||
use crate::{
|
use crate::{
|
||||||
matrix::{MATRIX_COLS, MATRIX_ROWS},
|
matrix::{MATRIX_COLS, MATRIX_ROWS},
|
||||||
render::{
|
render::{
|
||||||
BackgroundRenderResources, PRESSURE_CELL_COUNT, PressureFrame, WgpuBackgroundCallback,
|
BackgroundRenderResources, MarkerMode, PRESSURE_CELL_COUNT, PressureFrame,
|
||||||
|
WgpuBackgroundCallback,
|
||||||
},
|
},
|
||||||
ui::{
|
ui::{
|
||||||
ConfigPanelState, ConnectPanelState, FloatingPanelState, MatrixConfigState,
|
ConfigPanelState, ConnectPanelState, FloatingPanelState, MatrixConfigState,
|
||||||
@@ -15,6 +13,8 @@ use crate::{
|
|||||||
draw_scene_panel, draw_stats_panel,
|
draw_scene_panel, draw_stats_panel,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use eframe::{egui, egui_wgpu};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
const DATA_LOG_EVERY_FRAMES: u64 = 30;
|
const DATA_LOG_EVERY_FRAMES: u64 = 30;
|
||||||
|
|
||||||
@@ -53,6 +53,7 @@ impl EskinDesktopApp {
|
|||||||
.callback_resources
|
.callback_resources
|
||||||
.insert(BackgroundRenderResources::new(
|
.insert(BackgroundRenderResources::new(
|
||||||
&wgpu_state.device,
|
&wgpu_state.device,
|
||||||
|
&wgpu_state.queue,
|
||||||
&wgpu_state.target_format,
|
&wgpu_state.target_format,
|
||||||
MATRIX_ROWS,
|
MATRIX_ROWS,
|
||||||
MATRIX_COLS,
|
MATRIX_COLS,
|
||||||
@@ -90,6 +91,7 @@ impl EskinDesktopApp {
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
pressure: self.pressure_matrix,
|
pressure: self.pressure_matrix,
|
||||||
|
marker_mode: MarkerMode::Number,
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -243,7 +245,12 @@ impl EskinDesktopApp {
|
|||||||
draw_scene_panel(ctx, &mut self.scene_panel);
|
draw_scene_panel(ctx, &mut self.scene_panel);
|
||||||
draw_config_panel(ctx, &mut self.config_panel, &mut self.config_state);
|
draw_config_panel(ctx, &mut self.config_panel, &mut self.config_state);
|
||||||
draw_stats_panel(ctx, &mut self.stats_panel);
|
draw_stats_panel(ctx, &mut self.stats_panel);
|
||||||
draw_export_panel(ctx, &mut self.export_panel, &self.recorder, &mut self.export_path);
|
draw_export_panel(
|
||||||
|
ctx,
|
||||||
|
&mut self.export_panel,
|
||||||
|
&self.recorder,
|
||||||
|
&mut self.export_path,
|
||||||
|
);
|
||||||
draw_matrix_config_panel(ctx, &mut self.matrix_config_panel, &mut self.matrix_config);
|
draw_matrix_config_panel(ctx, &mut self.matrix_config_panel, &mut self.matrix_config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB |
@@ -1,11 +1,14 @@
|
|||||||
mod app;
|
mod app;
|
||||||
mod connection;
|
mod connection;
|
||||||
mod matrix;
|
mod matrix;
|
||||||
|
mod model;
|
||||||
|
mod recording;
|
||||||
mod render;
|
mod render;
|
||||||
|
mod resources;
|
||||||
mod serial_core;
|
mod serial_core;
|
||||||
|
mod texture;
|
||||||
mod theme;
|
mod theme;
|
||||||
mod ui;
|
mod ui;
|
||||||
mod recording;
|
|
||||||
mod utils;
|
mod utils;
|
||||||
use app::EskinDesktopApp;
|
use app::EskinDesktopApp;
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
|
|||||||
146
src/model.rs
Normal file
146
src/model.rs
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
use eframe::egui_wgpu::wgpu;
|
||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
use crate::texture;
|
||||||
|
pub trait Vertex {
|
||||||
|
fn desc<'a>() -> wgpu::VertexBufferLayout<'a>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
|
pub struct ModelVertex {
|
||||||
|
pub position: [f32; 3],
|
||||||
|
pub tex_coords: [f32; 2],
|
||||||
|
pub normal: [f32; 3],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Vertex for ModelVertex {
|
||||||
|
fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
|
||||||
|
use core::mem;
|
||||||
|
wgpu::VertexBufferLayout {
|
||||||
|
array_stride: mem::size_of::<ModelVertex>() 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,
|
||||||
|
},
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: mem::size_of::<[f32; 5]>() as wgpu::BufferAddress,
|
||||||
|
shader_location: 2,
|
||||||
|
format: wgpu::VertexFormat::Float32x3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Instance {
|
||||||
|
position: glam::Vec3,
|
||||||
|
rotation: glam::Quat,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Instance {
|
||||||
|
pub fn new(position: glam::Vec3, rotation: glam::Quat) -> Self {
|
||||||
|
Self { position, rotation }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_raw(&self) -> InstanceRaw {
|
||||||
|
InstanceRaw {
|
||||||
|
model: (glam::Mat4::from_translation(self.position)
|
||||||
|
* glam::Mat4::from_quat(self.rotation))
|
||||||
|
.to_cols_array_2d(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
|
pub struct InstanceRaw {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
model: [[f32; 4]; 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InstanceRaw {
|
||||||
|
pub fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
|
||||||
|
use core::mem;
|
||||||
|
wgpu::VertexBufferLayout {
|
||||||
|
array_stride: mem::size_of::<InstanceRaw>() as wgpu::BufferAddress,
|
||||||
|
// We need to switch from using a step mode of Vertex to Instance
|
||||||
|
// This means that our shaders will only change to use the next
|
||||||
|
// instance when the shader starts processing a new instance
|
||||||
|
step_mode: wgpu::VertexStepMode::Instance,
|
||||||
|
attributes: &[
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: 0,
|
||||||
|
// While our vertex shader only uses locations 0, and 1 now, in later tutorials we'll
|
||||||
|
// be using 2, 3, and 4, for Vertex. We'll start at slot 5 not conflict with them later
|
||||||
|
shader_location: 5,
|
||||||
|
format: wgpu::VertexFormat::Float32x4,
|
||||||
|
},
|
||||||
|
// A mat4 takes up 4 vertex slots as it is technically 4 vec4s. We need to define a slot
|
||||||
|
// for each vec4. We don't have to do this in code though.
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: mem::size_of::<[f32; 4]>() as wgpu::BufferAddress,
|
||||||
|
shader_location: 6,
|
||||||
|
format: wgpu::VertexFormat::Float32x4,
|
||||||
|
},
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: mem::size_of::<[f32; 8]>() as wgpu::BufferAddress,
|
||||||
|
shader_location: 7,
|
||||||
|
format: wgpu::VertexFormat::Float32x4,
|
||||||
|
},
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: mem::size_of::<[f32; 12]>() as wgpu::BufferAddress,
|
||||||
|
shader_location: 8,
|
||||||
|
format: wgpu::VertexFormat::Float32x4,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Model {
|
||||||
|
pub meshes: Vec<Mesh>,
|
||||||
|
pub materials: Vec<Material>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Material {
|
||||||
|
pub name: String,
|
||||||
|
pub diffuse_texture: texture::Texture,
|
||||||
|
pub bind_group: wgpu::BindGroup,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Mesh {
|
||||||
|
pub name: String,
|
||||||
|
pub vertex_buffer: wgpu::Buffer,
|
||||||
|
pub index_buffer: wgpu::Buffer,
|
||||||
|
pub num_elements: u32,
|
||||||
|
pub material: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait DrawModel<'a> {
|
||||||
|
fn draw_mesh(&mut self, mesh: &'a Mesh);
|
||||||
|
fn draw_mesh_instanced(&mut self, mesh: &'a Mesh, instances: Range<u32>);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> DrawModel<'b> for wgpu::RenderPass<'a>
|
||||||
|
where
|
||||||
|
'b: 'a,
|
||||||
|
{
|
||||||
|
fn draw_mesh(&mut self, mesh: &'b Mesh) {
|
||||||
|
self.draw_mesh_instanced(mesh, 0..1);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_mesh_instanced(&mut self, mesh: &'b Mesh, instances: Range<u32>) {
|
||||||
|
self.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
|
||||||
|
self.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
|
||||||
|
self.draw_indexed(0..mesh.num_elements, 0, instances);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -292,9 +292,11 @@ impl Recorder {
|
|||||||
|
|
||||||
let mut pressures = Vec::with_capacity(n_channels);
|
let mut pressures = Vec::with_capacity(n_channels);
|
||||||
for f in &fields[..n_channels] {
|
for f in &fields[..n_channels] {
|
||||||
pressures.push(f.parse::<u32>().with_context(|| {
|
pressures.push(
|
||||||
format!("line {}: bad channel value '{}'", lineno + 2, f)
|
f.parse::<u32>().with_context(|| {
|
||||||
})?);
|
format!("line {}: bad channel value '{}'", lineno + 2, f)
|
||||||
|
})?,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
let raw_ts = fields[n_channels]
|
let raw_ts = fields[n_channels]
|
||||||
.parse::<u64>()
|
.parse::<u64>()
|
||||||
@@ -320,7 +322,8 @@ impl Recorder {
|
|||||||
// Pretend the recording happened `last.timestamp_ms` ago
|
// Pretend the recording happened `last.timestamp_ms` ago
|
||||||
// so that elapsed_ms() would return that value.
|
// so that elapsed_ms() would return that value.
|
||||||
// We store a "fake" start by noting the offset.
|
// We store a "fake" start by noting the offset.
|
||||||
r.start = Some(Instant::now() - std::time::Duration::from_millis(last.timestamp_ms));
|
r.start =
|
||||||
|
Some(Instant::now() - std::time::Duration::from_millis(last.timestamp_ms));
|
||||||
r.paused_duration_ms = 0;
|
r.paused_duration_ms = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
216
src/render.rs
216
src/render.rs
@@ -1,11 +1,14 @@
|
|||||||
|
use crate::{
|
||||||
|
matrix::{MatrixLayout, build_view_projection, glyph_world_position},
|
||||||
|
model::{Instance, InstanceRaw, Model, ModelVertex, Vertex},
|
||||||
|
resources,
|
||||||
|
};
|
||||||
use eframe::{
|
use eframe::{
|
||||||
egui,
|
egui,
|
||||||
egui_wgpu::{self, wgpu},
|
egui_wgpu::{self, wgpu},
|
||||||
wgpu::util::DeviceExt,
|
wgpu::util::DeviceExt,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::matrix::{MatrixLayout, build_view_projection, glyph_world_position};
|
|
||||||
|
|
||||||
pub const PRESSURE_CELL_COUNT: usize =
|
pub const PRESSURE_CELL_COUNT: usize =
|
||||||
(crate::matrix::MATRIX_ROWS * crate::matrix::MATRIX_COLS) as usize;
|
(crate::matrix::MATRIX_ROWS * crate::matrix::MATRIX_COLS) as usize;
|
||||||
pub type PressureFrame = [[f32; 2]; PRESSURE_CELL_COUNT];
|
pub type PressureFrame = [[f32; 2]; PRESSURE_CELL_COUNT];
|
||||||
@@ -14,6 +17,13 @@ pub struct WgpuBackgroundCallback {
|
|||||||
pub width: f32,
|
pub width: f32,
|
||||||
pub height: f32,
|
pub height: f32,
|
||||||
pub pressure: PressureFrame,
|
pub pressure: PressureFrame,
|
||||||
|
pub marker_mode: MarkerMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum MarkerMode {
|
||||||
|
Number,
|
||||||
|
Dot,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl egui_wgpu::CallbackTrait for WgpuBackgroundCallback {
|
impl egui_wgpu::CallbackTrait for WgpuBackgroundCallback {
|
||||||
@@ -26,7 +36,13 @@ impl egui_wgpu::CallbackTrait for WgpuBackgroundCallback {
|
|||||||
resources: &mut egui_wgpu::CallbackResources,
|
resources: &mut egui_wgpu::CallbackResources,
|
||||||
) -> Vec<wgpu::CommandBuffer> {
|
) -> Vec<wgpu::CommandBuffer> {
|
||||||
let resources: &mut BackgroundRenderResources = resources.get_mut().unwrap();
|
let resources: &mut BackgroundRenderResources = resources.get_mut().unwrap();
|
||||||
resources.prepare(queue, self.width, self.height, &self.pressure);
|
resources.prepare(
|
||||||
|
queue,
|
||||||
|
self.width,
|
||||||
|
self.height,
|
||||||
|
&self.pressure,
|
||||||
|
self.marker_mode,
|
||||||
|
);
|
||||||
|
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
@@ -51,14 +67,20 @@ pub struct BackgroundRenderResources {
|
|||||||
uniform_bind_group: wgpu::BindGroup,
|
uniform_bind_group: wgpu::BindGroup,
|
||||||
background_pipeline: wgpu::RenderPipeline,
|
background_pipeline: wgpu::RenderPipeline,
|
||||||
glyph_pipeline: wgpu::RenderPipeline,
|
glyph_pipeline: wgpu::RenderPipeline,
|
||||||
|
dot_pipeline: wgpu::RenderPipeline,
|
||||||
|
model_pipeline: wgpu::RenderPipeline,
|
||||||
|
model: Option<Model>,
|
||||||
|
model_instance_buffer: wgpu::Buffer,
|
||||||
glyph_vertex_buffer: wgpu::Buffer,
|
glyph_vertex_buffer: wgpu::Buffer,
|
||||||
glyph_instance_buffer: wgpu::Buffer,
|
glyph_instance_buffer: wgpu::Buffer,
|
||||||
glyph_instances: Vec<GlyphInstance>,
|
glyph_instances: Vec<GlyphInstance>,
|
||||||
|
marker_mode: MarkerMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BackgroundRenderResources {
|
impl BackgroundRenderResources {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
device: &wgpu::Device,
|
device: &wgpu::Device,
|
||||||
|
_queue: &wgpu::Queue,
|
||||||
target_format: &wgpu::TextureFormat,
|
target_format: &wgpu::TextureFormat,
|
||||||
rows: u32,
|
rows: u32,
|
||||||
cols: u32,
|
cols: u32,
|
||||||
@@ -96,20 +118,86 @@ impl BackgroundRenderResources {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
label: Some("Pressure Matrix Shader"),
|
label: Some("Pressure Matrix Background Shader"),
|
||||||
source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()),
|
source: wgpu::ShaderSource::Wgsl(include_str!("../static/wgsl/shader.wgsl").into()),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// let glyph_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
|
// label: Some("Pressure Matrix Glyph Shader"),
|
||||||
|
// source: wgpu::ShaderSource::Wgsl(include_str!("../static/wgsl/shader.wgsl").into()),
|
||||||
|
// });
|
||||||
|
|
||||||
|
// let dot_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
|
// label: Some("Pressure Matrix Dot Shader"),
|
||||||
|
// source: wgpu::ShaderSource::Wgsl(include_str!("../static/wgsl/shader.wgsl").into()),
|
||||||
|
// });
|
||||||
|
|
||||||
|
let texture_bind_group_layout =
|
||||||
|
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||||
|
label: Some("model_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 pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||||
label: Some("Pressure Matrix Pipeline Layout"),
|
label: Some("Pressure Matrix Pipeline Layout"),
|
||||||
bind_group_layouts: &[Some(&uniform_bind_group_layout)],
|
bind_group_layouts: &[Some(&uniform_bind_group_layout)],
|
||||||
immediate_size: 0,
|
immediate_size: 0,
|
||||||
});
|
});
|
||||||
|
let model_pipeline_layout =
|
||||||
|
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||||
|
label: Some("Hand Model Pipeline Layout"),
|
||||||
|
bind_group_layouts: &[
|
||||||
|
Some(&uniform_bind_group_layout),
|
||||||
|
Some(&texture_bind_group_layout),
|
||||||
|
],
|
||||||
|
immediate_size: 0,
|
||||||
|
});
|
||||||
|
|
||||||
let background_pipeline =
|
let background_pipeline =
|
||||||
create_background_pipeline(device, target_format, &shader, &pipeline_layout);
|
create_background_pipeline(device, target_format, &shader, &pipeline_layout);
|
||||||
let glyph_pipeline =
|
let glyph_pipeline =
|
||||||
create_glyph_pipeline(device, target_format, &shader, &pipeline_layout);
|
create_glyph_pipeline(device, target_format, &shader, &pipeline_layout);
|
||||||
|
let dot_pipeline = create_dot_pipeline(device, target_format, &shader, &pipeline_layout);
|
||||||
|
|
||||||
|
let model_pipeline =
|
||||||
|
create_model_pipeline(device, target_format, &shader, &model_pipeline_layout);
|
||||||
|
|
||||||
|
let model =
|
||||||
|
match resources::load_model("cube.obj", device, _queue, &texture_bind_group_layout) {
|
||||||
|
Ok(model) => Some(model),
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!("failed to load cube.obj: {err:#}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let model_instance = Instance::new(
|
||||||
|
glam::Vec3::new(0.0, -2.0, 12.0),
|
||||||
|
glam::Quat::from_rotation_y(0.65) * glam::Quat::from_rotation_x(-0.35),
|
||||||
|
)
|
||||||
|
.to_raw();
|
||||||
|
let model_instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||||
|
label: Some("Hand Model Instance Buffer"),
|
||||||
|
contents: bytemuck::cast_slice(&[model_instance]),
|
||||||
|
usage: wgpu::BufferUsages::VERTEX,
|
||||||
|
});
|
||||||
|
|
||||||
let glyph_vertices = [
|
let glyph_vertices = [
|
||||||
GlyphVertex {
|
GlyphVertex {
|
||||||
@@ -144,16 +232,29 @@ impl BackgroundRenderResources {
|
|||||||
uniform_bind_group,
|
uniform_bind_group,
|
||||||
background_pipeline,
|
background_pipeline,
|
||||||
glyph_pipeline,
|
glyph_pipeline,
|
||||||
|
dot_pipeline,
|
||||||
|
model_pipeline,
|
||||||
|
model,
|
||||||
|
model_instance_buffer,
|
||||||
glyph_vertex_buffer,
|
glyph_vertex_buffer,
|
||||||
glyph_instance_buffer,
|
glyph_instance_buffer,
|
||||||
glyph_instances,
|
glyph_instances,
|
||||||
|
marker_mode: MarkerMode::Number,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare(&mut self, queue: &wgpu::Queue, width: f32, height: f32, pressure: &PressureFrame) {
|
fn prepare(
|
||||||
|
&mut self,
|
||||||
|
queue: &wgpu::Queue,
|
||||||
|
width: f32,
|
||||||
|
height: f32,
|
||||||
|
pressure: &PressureFrame,
|
||||||
|
marker_mode: MarkerMode,
|
||||||
|
) {
|
||||||
let aspect = width / height.max(1.0);
|
let aspect = width / height.max(1.0);
|
||||||
self.uniform =
|
self.uniform =
|
||||||
MatrixUniform::new(width, height, build_view_projection(aspect, &self.layout));
|
MatrixUniform::new(width, height, build_view_projection(aspect, &self.layout));
|
||||||
|
self.marker_mode = marker_mode;
|
||||||
queue.write_buffer(
|
queue.write_buffer(
|
||||||
&self.uniform_buffer,
|
&self.uniform_buffer,
|
||||||
0,
|
0,
|
||||||
@@ -180,7 +281,27 @@ impl BackgroundRenderResources {
|
|||||||
render_pass.set_pipeline(&self.background_pipeline);
|
render_pass.set_pipeline(&self.background_pipeline);
|
||||||
render_pass.draw(0..3, 0..1);
|
render_pass.draw(0..3, 0..1);
|
||||||
|
|
||||||
render_pass.set_pipeline(&self.glyph_pipeline);
|
if let Some(model) = &self.model {
|
||||||
|
render_pass.set_pipeline(&self.model_pipeline);
|
||||||
|
render_pass.set_vertex_buffer(1, self.model_instance_buffer.slice(..));
|
||||||
|
for mesh in &model.meshes {
|
||||||
|
if let Some(material) = model.materials.get(mesh.material) {
|
||||||
|
render_pass.set_bind_group(1, &material.bind_group, &[]);
|
||||||
|
}
|
||||||
|
render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
|
||||||
|
render_pass
|
||||||
|
.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
|
||||||
|
render_pass.draw_indexed(0..mesh.num_elements, 0, 0..1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// let marker_pipeline = match self.marker_mode {
|
||||||
|
// MarkerMode::Number => &self.glyph_pipeline,
|
||||||
|
// MarkerMode::Dot => &self.dot_pipeline,
|
||||||
|
// };
|
||||||
|
let marker_pipeline = &self.dot_pipeline;
|
||||||
|
|
||||||
|
render_pass.set_pipeline(marker_pipeline);
|
||||||
render_pass.set_vertex_buffer(0, self.glyph_vertex_buffer.slice(..));
|
render_pass.set_vertex_buffer(0, self.glyph_vertex_buffer.slice(..));
|
||||||
render_pass.set_vertex_buffer(1, self.glyph_instance_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);
|
render_pass.draw(0..6, 0..self.glyph_instances.len() as u32);
|
||||||
@@ -253,6 +374,87 @@ fn create_glyph_pipeline(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_model_pipeline(
|
||||||
|
device: &wgpu::Device,
|
||||||
|
target_format: &wgpu::TextureFormat,
|
||||||
|
shader: &wgpu::ShaderModule,
|
||||||
|
layout: &wgpu::PipelineLayout,
|
||||||
|
) -> wgpu::RenderPipeline {
|
||||||
|
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
|
label: Some("Hand Model Pipeline"),
|
||||||
|
layout: Some(layout),
|
||||||
|
vertex: wgpu::VertexState {
|
||||||
|
module: shader,
|
||||||
|
entry_point: Some("vs_model"),
|
||||||
|
compilation_options: Default::default(),
|
||||||
|
buffers: &[ModelVertex::desc(), InstanceRaw::desc()],
|
||||||
|
},
|
||||||
|
fragment: Some(wgpu::FragmentState {
|
||||||
|
module: shader,
|
||||||
|
entry_point: Some("fs_model"),
|
||||||
|
compilation_options: Default::default(),
|
||||||
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
|
format: *target_format,
|
||||||
|
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: None,
|
||||||
|
polygon_mode: wgpu::PolygonMode::Fill,
|
||||||
|
unclipped_depth: false,
|
||||||
|
conservative: false,
|
||||||
|
},
|
||||||
|
depth_stencil: None,
|
||||||
|
multisample: wgpu::MultisampleState {
|
||||||
|
count: 1,
|
||||||
|
mask: !0,
|
||||||
|
alpha_to_coverage_enabled: false,
|
||||||
|
},
|
||||||
|
multiview_mask: None,
|
||||||
|
cache: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_dot_pipeline(
|
||||||
|
device: &wgpu::Device,
|
||||||
|
target_format: &wgpu::TextureFormat,
|
||||||
|
shader: &wgpu::ShaderModule,
|
||||||
|
layout: &wgpu::PipelineLayout,
|
||||||
|
) -> wgpu::RenderPipeline {
|
||||||
|
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
|
label: Some("Pressure Matrix Dot Pipeline"),
|
||||||
|
layout: Some(layout),
|
||||||
|
vertex: wgpu::VertexState {
|
||||||
|
module: shader,
|
||||||
|
entry_point: Some("vs_dot"),
|
||||||
|
compilation_options: Default::default(),
|
||||||
|
buffers: &[GlyphVertex::desc(), GlyphInstance::desc()],
|
||||||
|
},
|
||||||
|
fragment: Some(wgpu::FragmentState {
|
||||||
|
module: shader,
|
||||||
|
entry_point: Some("fs_dot"),
|
||||||
|
compilation_options: Default::default(),
|
||||||
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
|
format: *target_format,
|
||||||
|
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||||
|
write_mask: wgpu::ColorWrites::ALL,
|
||||||
|
})],
|
||||||
|
}),
|
||||||
|
primitive: wgpu::PrimitiveState::default(),
|
||||||
|
depth_stencil: None,
|
||||||
|
multisample: wgpu::MultisampleState::default(),
|
||||||
|
multiview_mask: None,
|
||||||
|
cache: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn build_glyph_instances(
|
fn build_glyph_instances(
|
||||||
rows: u32,
|
rows: u32,
|
||||||
cols: u32,
|
cols: u32,
|
||||||
|
|||||||
128
src/resources.rs
Normal file
128
src/resources.rs
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
use crate::{model, texture};
|
||||||
|
use eframe::wgpu;
|
||||||
|
use eframe::wgpu::util::DeviceExt;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
fn resource_path(file_name: &str) -> PathBuf {
|
||||||
|
Path::new(env!("RESOURCE_DIR")).join(file_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_string(file_name: &str) -> anyhow::Result<String> {
|
||||||
|
Ok(std::fs::read_to_string(resource_path(file_name))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_binary(file_name: &str) -> anyhow::Result<Vec<u8>> {
|
||||||
|
Ok(std::fs::read(resource_path(file_name))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_texture(
|
||||||
|
file_name: &str,
|
||||||
|
device: &wgpu::Device,
|
||||||
|
queue: &wgpu::Queue,
|
||||||
|
) -> anyhow::Result<texture::Texture> {
|
||||||
|
let data = load_binary(file_name)?;
|
||||||
|
texture::Texture::from_bytes(device, queue, &data, file_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_model(
|
||||||
|
file_name: &str,
|
||||||
|
device: &wgpu::Device,
|
||||||
|
queue: &wgpu::Queue,
|
||||||
|
texture_bind_group_layout: &wgpu::BindGroupLayout,
|
||||||
|
) -> anyhow::Result<model::Model> {
|
||||||
|
let path = resource_path(file_name);
|
||||||
|
let (models, obj_materials) = tobj::load_obj(
|
||||||
|
&path,
|
||||||
|
&tobj::LoadOptions {
|
||||||
|
single_index: true,
|
||||||
|
triangulate: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut materials = Vec::new();
|
||||||
|
for material in obj_materials? {
|
||||||
|
let diffuse_texture = match material.diffuse_texture.as_deref() {
|
||||||
|
Some(texture_path) => load_texture(texture_path, device, queue)?,
|
||||||
|
None => texture::Texture::from_rgba8(
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
&[255, 255, 255, 255],
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
&material.name,
|
||||||
|
)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
|
label: Some(&format!("{} Texture Bind Group", material.name)),
|
||||||
|
layout: texture_bind_group_layout,
|
||||||
|
entries: &[
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 0,
|
||||||
|
resource: wgpu::BindingResource::TextureView(&diffuse_texture.view),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 1,
|
||||||
|
resource: wgpu::BindingResource::Sampler(&diffuse_texture.sampler),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
materials.push(model::Material {
|
||||||
|
name: material.name,
|
||||||
|
diffuse_texture,
|
||||||
|
bind_group,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let meshes = models
|
||||||
|
.into_iter()
|
||||||
|
.map(|m| {
|
||||||
|
let vertices = (0..m.mesh.positions.len() / 3)
|
||||||
|
.map(|i| model::ModelVertex {
|
||||||
|
position: [
|
||||||
|
m.mesh.positions[i * 3],
|
||||||
|
m.mesh.positions[i * 3 + 1],
|
||||||
|
m.mesh.positions[i * 3 + 2],
|
||||||
|
],
|
||||||
|
tex_coords: if m.mesh.texcoords.len() >= i * 2 + 2 {
|
||||||
|
[m.mesh.texcoords[i * 2], m.mesh.texcoords[i * 2 + 1]]
|
||||||
|
} else {
|
||||||
|
[0.0, 0.0]
|
||||||
|
},
|
||||||
|
normal: if m.mesh.normals.len() >= i * 3 + 3 {
|
||||||
|
[
|
||||||
|
m.mesh.normals[i * 3],
|
||||||
|
m.mesh.normals[i * 3 + 1],
|
||||||
|
m.mesh.normals[i * 3 + 2],
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
[0.0, 1.0, 0.0]
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||||
|
label: Some(&format!("{} Vertex Buffer", m.name)),
|
||||||
|
contents: bytemuck::cast_slice(&vertices),
|
||||||
|
usage: wgpu::BufferUsages::VERTEX,
|
||||||
|
});
|
||||||
|
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||||
|
label: Some(&format!("{} Index Buffer", m.name)),
|
||||||
|
contents: bytemuck::cast_slice(&m.mesh.indices),
|
||||||
|
usage: wgpu::BufferUsages::INDEX,
|
||||||
|
});
|
||||||
|
|
||||||
|
model::Mesh {
|
||||||
|
name: m.name,
|
||||||
|
vertex_buffer,
|
||||||
|
index_buffer,
|
||||||
|
num_elements: m.mesh.indices.len() as u32,
|
||||||
|
material: m.mesh.material_id.unwrap_or(0),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(model::Model { meshes, materials })
|
||||||
|
}
|
||||||
117
src/texture.rs
117
src/texture.rs
@@ -1,8 +1,12 @@
|
|||||||
use anyhow::*;
|
#![allow(dead_code)]
|
||||||
use eframe::egui_wgpu::{self, wgpu};
|
|
||||||
use image::GenericImageView;
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
use anyhow::*;
|
||||||
|
use eframe::{
|
||||||
|
egui,
|
||||||
|
egui_wgpu::{self, wgpu},
|
||||||
|
wgpu::util::DeviceExt,
|
||||||
|
};
|
||||||
|
use image::GenericImageView;
|
||||||
pub struct Texture {
|
pub struct Texture {
|
||||||
pub texture: wgpu::Texture,
|
pub texture: wgpu::Texture,
|
||||||
pub view: wgpu::TextureView,
|
pub view: wgpu::TextureView,
|
||||||
@@ -10,6 +14,51 @@ pub struct Texture {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Texture {
|
impl Texture {
|
||||||
|
pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
|
||||||
|
|
||||||
|
pub fn create_depth_texture(
|
||||||
|
device: &wgpu::Device,
|
||||||
|
config: &wgpu::SurfaceConfiguration,
|
||||||
|
label: &str,
|
||||||
|
) -> Self {
|
||||||
|
let size = wgpu::Extent3d {
|
||||||
|
width: config.width,
|
||||||
|
height: config.height,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
};
|
||||||
|
let desc = wgpu::TextureDescriptor {
|
||||||
|
label: Some(label),
|
||||||
|
size,
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
|
format: Self::DEPTH_FORMAT,
|
||||||
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
|
||||||
|
view_formats: &[],
|
||||||
|
};
|
||||||
|
let texture = device.create_texture(&desc);
|
||||||
|
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
|
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||||
|
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
||||||
|
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
||||||
|
address_mode_w: wgpu::AddressMode::ClampToEdge,
|
||||||
|
mag_filter: wgpu::FilterMode::Linear,
|
||||||
|
min_filter: wgpu::FilterMode::Linear,
|
||||||
|
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
|
||||||
|
compare: Some(wgpu::CompareFunction::LessEqual),
|
||||||
|
lod_min_clamp: 0.0,
|
||||||
|
lod_max_clamp: 200.0,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
texture,
|
||||||
|
view,
|
||||||
|
sampler,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn from_bytes(
|
pub fn from_bytes(
|
||||||
device: &wgpu::Device,
|
device: &wgpu::Device,
|
||||||
queue: &wgpu::Queue,
|
queue: &wgpu::Queue,
|
||||||
@@ -26,8 +75,8 @@ impl Texture {
|
|||||||
img: &image::DynamicImage,
|
img: &image::DynamicImage,
|
||||||
label: Option<&str>,
|
label: Option<&str>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let rgba = img.to_rgba8();
|
|
||||||
let dimensions = img.dimensions();
|
let dimensions = img.dimensions();
|
||||||
|
let rgba = img.to_rgba8();
|
||||||
|
|
||||||
let size = wgpu::Extent3d {
|
let size = wgpu::Extent3d {
|
||||||
width: dimensions.0,
|
width: dimensions.0,
|
||||||
@@ -78,4 +127,62 @@ impl Texture {
|
|||||||
sampler,
|
sampler,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn from_rgba8(
|
||||||
|
device: &wgpu::Device,
|
||||||
|
queue: &wgpu::Queue,
|
||||||
|
rgba: &[u8],
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
label: &str,
|
||||||
|
) -> Result<Self> {
|
||||||
|
let size = wgpu::Extent3d {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
};
|
||||||
|
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||||
|
label: Some(label),
|
||||||
|
size,
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
|
format: wgpu::TextureFormat::Rgba8UnormSrgb,
|
||||||
|
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||||||
|
view_formats: &[],
|
||||||
|
});
|
||||||
|
|
||||||
|
queue.write_texture(
|
||||||
|
wgpu::TexelCopyTextureInfo {
|
||||||
|
aspect: wgpu::TextureAspect::All,
|
||||||
|
texture: &texture,
|
||||||
|
mip_level: 0,
|
||||||
|
origin: wgpu::Origin3d::ZERO,
|
||||||
|
},
|
||||||
|
rgba,
|
||||||
|
wgpu::TexelCopyBufferLayout {
|
||||||
|
offset: 0,
|
||||||
|
bytes_per_row: Some(4 * width),
|
||||||
|
rows_per_image: Some(height),
|
||||||
|
},
|
||||||
|
size,
|
||||||
|
);
|
||||||
|
|
||||||
|
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
|
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||||
|
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
||||||
|
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
||||||
|
address_mode_w: wgpu::AddressMode::ClampToEdge,
|
||||||
|
mag_filter: wgpu::FilterMode::Linear,
|
||||||
|
min_filter: wgpu::FilterMode::Nearest,
|
||||||
|
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
texture,
|
||||||
|
view,
|
||||||
|
sampler,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
24
src/theme.rs
24
src/theme.rs
@@ -16,24 +16,24 @@ pub struct AppTheme {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub const ONE_DARK_PRO: AppTheme = AppTheme {
|
pub const ONE_DARK_PRO: AppTheme = AppTheme {
|
||||||
bg: egui::Color32::from_rgb(40, 44, 52), // #282C34 editor background
|
bg: egui::Color32::from_rgb(40, 44, 52), // #282C34 editor background
|
||||||
panel: egui::Color32::from_rgb(33, 37, 43), // #21252B sidebar/darker
|
panel: egui::Color32::from_rgb(33, 37, 43), // #21252B sidebar/darker
|
||||||
panel_strong: egui::Color32::from_rgb(44, 49, 58), // #2C313A slightly lighter
|
panel_strong: egui::Color32::from_rgb(44, 49, 58), // #2C313A slightly lighter
|
||||||
panel_deep: egui::Color32::from_rgb(27, 31, 39), // #1B1F27 darkest
|
panel_deep: egui::Color32::from_rgb(27, 31, 39), // #1B1F27 darkest
|
||||||
border: egui::Color32::from_rgb(62, 68, 81), // #3E4451 border
|
border: egui::Color32::from_rgb(62, 68, 81), // #3E4451 border
|
||||||
border_soft: egui::Color32::from_rgb(44, 49, 58), // #2C313A soft border
|
border_soft: egui::Color32::from_rgb(44, 49, 58), // #2C313A soft border
|
||||||
text: egui::Color32::from_rgb(171, 178, 191), // #ABB2BF default text
|
text: egui::Color32::from_rgb(171, 178, 191), // #ABB2BF default text
|
||||||
text_dim: egui::Color32::from_rgb(92, 99, 112), // #5C6370 dimmed text
|
text_dim: egui::Color32::from_rgb(92, 99, 112), // #5C6370 dimmed text
|
||||||
accent: egui::Color32::from_rgb(198, 120, 221), // #C678DD purple (signature)
|
accent: egui::Color32::from_rgb(198, 120, 221), // #C678DD purple (signature)
|
||||||
accent_hot: egui::Color32::from_rgb(229, 192, 123), // #E5C07B yellow/gold
|
accent_hot: egui::Color32::from_rgb(229, 192, 123), // #E5C07B yellow/gold
|
||||||
radius: 3,
|
radius: 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Additional One Dark Pro accent colors (standalone constants)
|
// Additional One Dark Pro accent colors (standalone constants)
|
||||||
pub const ACCENT_GREEN: egui::Color32 = egui::Color32::from_rgb(152, 195, 121); // #98C379
|
pub const ACCENT_GREEN: egui::Color32 = egui::Color32::from_rgb(152, 195, 121); // #98C379
|
||||||
pub const ACCENT_RED: egui::Color32 = egui::Color32::from_rgb(224, 108, 117); // #E06C75
|
pub const ACCENT_RED: egui::Color32 = egui::Color32::from_rgb(224, 108, 117); // #E06C75
|
||||||
pub const ACCENT_BLUE: egui::Color32 = egui::Color32::from_rgb(97, 175, 239); // #61AFEF
|
pub const ACCENT_BLUE: egui::Color32 = egui::Color32::from_rgb(97, 175, 239); // #61AFEF
|
||||||
pub const ACCENT_CYAN: egui::Color32 = egui::Color32::from_rgb(86, 182, 194); // #56B6C2
|
pub const ACCENT_CYAN: egui::Color32 = egui::Color32::from_rgb(86, 182, 194); // #56B6C2
|
||||||
pub const ACCENT_ORANGE: egui::Color32 = egui::Color32::from_rgb(209, 154, 102); // #D19A66
|
pub const ACCENT_ORANGE: egui::Color32 = egui::Color32::from_rgb(209, 154, 102); // #D19A66
|
||||||
|
|
||||||
pub fn apply_theme(ctx: &egui::Context, theme: &AppTheme) {
|
pub fn apply_theme(ctx: &egui::Context, theme: &AppTheme) {
|
||||||
|
|||||||
48
src/ui.rs
48
src/ui.rs
@@ -3,7 +3,10 @@ use eframe::egui;
|
|||||||
use crate::{
|
use crate::{
|
||||||
connection::{ConnectionManager, ConnectionState},
|
connection::{ConnectionManager, ConnectionState},
|
||||||
recording::Recorder,
|
recording::Recorder,
|
||||||
theme::{ONE_DARK_PRO, ACCENT_BLUE, ACCENT_GREEN, ACCENT_ORANGE, ACCENT_RED, accent_text, dim_text, group_frame, panel_frame, tag_button},
|
theme::{
|
||||||
|
ACCENT_BLUE, ACCENT_GREEN, ACCENT_ORANGE, ACCENT_RED, ONE_DARK_PRO, accent_text, dim_text,
|
||||||
|
group_frame, panel_frame, tag_button,
|
||||||
|
},
|
||||||
utils::serial_enum,
|
utils::serial_enum,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -977,12 +980,7 @@ fn paint_integrated_center_panel(
|
|||||||
// ── Signal Chart (sparkline) ───────────────────────────────────────────
|
// ── Signal Chart (sparkline) ───────────────────────────────────────────
|
||||||
|
|
||||||
/// Draw a mini sparkline chart with current/min/max labels.
|
/// Draw a mini sparkline chart with current/min/max labels.
|
||||||
pub fn draw_signal_chart(
|
pub fn draw_signal_chart(ui: &mut egui::Ui, values: &[f32], label: &str, color: egui::Color32) {
|
||||||
ui: &mut egui::Ui,
|
|
||||||
values: &[f32],
|
|
||||||
label: &str,
|
|
||||||
color: egui::Color32,
|
|
||||||
) {
|
|
||||||
let chart_height = 48.0;
|
let chart_height = 48.0;
|
||||||
let chart_width = ui.available_width().min(200.0);
|
let chart_width = ui.available_width().min(200.0);
|
||||||
|
|
||||||
@@ -991,16 +989,20 @@ pub fn draw_signal_chart(
|
|||||||
ui.colored_label(color, label);
|
ui.colored_label(color, label);
|
||||||
if let (Some(&last), Some(&min), Some(&max)) = (
|
if let (Some(&last), Some(&min), Some(&max)) = (
|
||||||
values.last(),
|
values.last(),
|
||||||
values.iter().min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)),
|
values
|
||||||
values.iter().max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)),
|
.iter()
|
||||||
|
.min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)),
|
||||||
|
values
|
||||||
|
.iter()
|
||||||
|
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)),
|
||||||
) {
|
) {
|
||||||
ui.colored_label(ONE_DARK_PRO.text, format!("{:.0}", last));
|
ui.colored_label(ONE_DARK_PRO.text, format!("{:.0}", last));
|
||||||
ui.colored_label(ONE_DARK_PRO.text_dim, format!("↓{:.0} ↑{:.0}", min, max));
|
ui.colored_label(ONE_DARK_PRO.text_dim, format!("↓{:.0} ↑{:.0}", min, max));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let (rect, _response) = ui
|
let (rect, _response) =
|
||||||
.allocate_exact_size(egui::vec2(chart_width, chart_height), egui::Sense::hover());
|
ui.allocate_exact_size(egui::vec2(chart_width, chart_height), egui::Sense::hover());
|
||||||
|
|
||||||
if values.len() < 2 {
|
if values.len() < 2 {
|
||||||
return;
|
return;
|
||||||
@@ -1038,11 +1040,7 @@ pub fn draw_signal_chart(
|
|||||||
// ── Recording Toolbar ──────────────────────────────────────────────────
|
// ── Recording Toolbar ──────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Draw recording controls inside the connect panel.
|
/// Draw recording controls inside the connect panel.
|
||||||
pub fn draw_recording_toolbar(
|
pub fn draw_recording_toolbar(ui: &mut egui::Ui, recorder: &Recorder, export_path: &mut String) {
|
||||||
ui: &mut egui::Ui,
|
|
||||||
recorder: &Recorder,
|
|
||||||
export_path: &mut String,
|
|
||||||
) {
|
|
||||||
let is_recording = recorder.is_recording();
|
let is_recording = recorder.is_recording();
|
||||||
let frame_count = recorder.frame_count();
|
let frame_count = recorder.frame_count();
|
||||||
let duration_ms = recorder.duration_ms();
|
let duration_ms = recorder.duration_ms();
|
||||||
@@ -1203,9 +1201,15 @@ pub fn draw_matrix_config_panel(
|
|||||||
ui.colored_label(dim_text(), "矩阵尺寸");
|
ui.colored_label(dim_text(), "矩阵尺寸");
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
ui.colored_label(ONE_DARK_PRO.text, "行");
|
ui.colored_label(ONE_DARK_PRO.text, "行");
|
||||||
ui.add_sized(egui::vec2(56.0, 20.0), egui::DragValue::new(&mut config.rows).range(1..=128));
|
ui.add_sized(
|
||||||
|
egui::vec2(56.0, 20.0),
|
||||||
|
egui::DragValue::new(&mut config.rows).range(1..=128),
|
||||||
|
);
|
||||||
ui.colored_label(ONE_DARK_PRO.text, "列");
|
ui.colored_label(ONE_DARK_PRO.text, "列");
|
||||||
ui.add_sized(egui::vec2(56.0, 20.0), egui::DragValue::new(&mut config.cols).range(1..=128));
|
ui.add_sized(
|
||||||
|
egui::vec2(56.0, 20.0),
|
||||||
|
egui::DragValue::new(&mut config.cols).range(1..=128),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.add_space(4.0);
|
ui.add_space(4.0);
|
||||||
@@ -1235,12 +1239,16 @@ pub fn draw_matrix_config_panel(
|
|||||||
ui.colored_label(ONE_DARK_PRO.text, "最小");
|
ui.colored_label(ONE_DARK_PRO.text, "最小");
|
||||||
ui.add_sized(
|
ui.add_sized(
|
||||||
egui::vec2(72.0, 20.0),
|
egui::vec2(72.0, 20.0),
|
||||||
egui::DragValue::new(&mut config.color_min).range(0.0..=10000.0).speed(100.0),
|
egui::DragValue::new(&mut config.color_min)
|
||||||
|
.range(0.0..=10000.0)
|
||||||
|
.speed(100.0),
|
||||||
);
|
);
|
||||||
ui.colored_label(ONE_DARK_PRO.text, "最大");
|
ui.colored_label(ONE_DARK_PRO.text, "最大");
|
||||||
ui.add_sized(
|
ui.add_sized(
|
||||||
egui::vec2(72.0, 20.0),
|
egui::vec2(72.0, 20.0),
|
||||||
egui::DragValue::new(&mut config.color_max).range(1.0..=10000.0).speed(100.0),
|
egui::DragValue::new(&mut config.color_max)
|
||||||
|
.range(1.0..=10000.0)
|
||||||
|
.speed(100.0),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
280
src/shader.wgsl → static/wgsl/shader.wgsl
Executable file → Normal file
280
src/shader.wgsl → static/wgsl/shader.wgsl
Executable file → Normal file
@@ -8,50 +8,16 @@ struct MatrixUniform {
|
|||||||
@group(0) @binding(0)
|
@group(0) @binding(0)
|
||||||
var<uniform> u: MatrixUniform;
|
var<uniform> u: MatrixUniform;
|
||||||
|
|
||||||
struct BackgroundVertexOutput {
|
@group(1) @binding(0)
|
||||||
@builtin(position) clip_position: vec4f,
|
var model_texture: texture_2d<f32>;
|
||||||
}
|
|
||||||
|
|
||||||
struct GlyphVertexInput {
|
@group(1) @binding(1)
|
||||||
@location(0) local: vec2f,
|
var model_sampler: sampler;
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
@location(2) display_value: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn saturate(value: f32) -> f32 {
|
fn saturate(value: f32) -> f32 {
|
||||||
return clamp(value, 0.0, 1.0);
|
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 rect_alpha(point: vec2f, center: vec2f, half_size: vec2f) -> f32 {
|
|
||||||
let delta = abs(point - center) - half_size;
|
|
||||||
let outside = length(max(delta, vec2f(0.0, 0.0)));
|
|
||||||
let inside = min(max(delta.x, delta.y), 0.0);
|
|
||||||
let dist = outside + inside;
|
|
||||||
return 1.0 - smoothstep(0.015, 0.045, dist);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn range_stop_color(index: u32) -> vec3f {
|
fn range_stop_color(index: u32) -> vec3f {
|
||||||
switch index {
|
switch index {
|
||||||
case 0u: {
|
case 0u: {
|
||||||
@@ -86,6 +52,90 @@ fn sample_range_color(value: f32) -> vec3f {
|
|||||||
return mix(range_stop_color(2u), range_stop_color(3u), local);
|
return mix(range_stop_color(2u), range_stop_color(3u), local);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// background
|
||||||
|
struct BackgroundVertexOutput {
|
||||||
|
@builtin(position) clip_position: vec4f,
|
||||||
|
}
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
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: 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)));
|
||||||
|
|
||||||
|
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, 4>(0.0, 0.33, 0.66, 1.0);
|
||||||
|
for (var index = 0u; index < 4u; 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// glyph
|
||||||
|
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,
|
||||||
|
@location(2) display_value: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rect_alpha(point: vec2f, center: vec2f, half_size: vec2f) -> f32 {
|
||||||
|
let delta = abs(point - center) - half_size;
|
||||||
|
let outside = length(max(delta, vec2f(0.0, 0.0)));
|
||||||
|
let inside = min(max(delta.x, delta.y), 0.0);
|
||||||
|
let dist = outside + inside;
|
||||||
|
return 1.0 - smoothstep(0.015, 0.045, dist);
|
||||||
|
}
|
||||||
|
|
||||||
fn digit_segment_on(digit: u32, segment: u32) -> bool {
|
fn digit_segment_on(digit: u32, segment: u32) -> bool {
|
||||||
switch digit {
|
switch digit {
|
||||||
case 0u: { return segment != 6u; }
|
case 0u: { return segment != 6u; }
|
||||||
@@ -183,58 +233,6 @@ fn number_alpha(local: vec2f, display_value: f32) -> f32 {
|
|||||||
return alpha;
|
return alpha;
|
||||||
}
|
}
|
||||||
|
|
||||||
@vertex
|
|
||||||
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: 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)));
|
|
||||||
|
|
||||||
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, 4>(0.0, 0.33, 0.66, 1.0);
|
|
||||||
for (var index = 0u; index < 4u; 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
|
@vertex
|
||||||
fn vs_glyph(vertex: GlyphVertexInput, instance: GlyphInstanceInput) -> GlyphVertexOutput {
|
fn vs_glyph(vertex: GlyphVertexInput, instance: GlyphInstanceInput) -> GlyphVertexOutput {
|
||||||
let center = u.view_proj * vec4f(instance.world_position.xyz, 1.0);
|
let center = u.view_proj * vec4f(instance.world_position.xyz, 1.0);
|
||||||
@@ -256,3 +254,105 @@ fn fs_glyph(in: GlyphVertexOutput) -> @location(0) vec4f {
|
|||||||
let color = sample_range_color(in.intensity) * mix(0.82, 1.16, saturate(in.intensity));
|
let color = sample_range_color(in.intensity) * mix(0.82, 1.16, saturate(in.intensity));
|
||||||
return vec4f(color, alpha);
|
return vec4f(color, alpha);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// dot
|
||||||
|
struct DotVertexInput {
|
||||||
|
@location(0) local: vec2f,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DotInstanceInput {
|
||||||
|
@location(1) world_position: vec4f,
|
||||||
|
@location(2) style: vec4f
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DotVertexOutput {
|
||||||
|
@builtin(position) clip_position: vec4f,
|
||||||
|
@location(0) local: vec2f,
|
||||||
|
@location(1) intensity: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn circle_alpha(local: vec2f, radius: f32, softness: f32) -> f32 {
|
||||||
|
let dist = length(local);
|
||||||
|
return 1.0 - smoothstep(radius, radius + softness, dist);
|
||||||
|
}
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vs_dot(vertex: DotVertexInput, instance: DotInstanceInput) -> DotVertexOutput {
|
||||||
|
let center = u.view_proj * vec4f(instance.world_position.xyz, 1.0);
|
||||||
|
let intensity = saturate(instance.style.x);
|
||||||
|
let shaped = pow(intensity, 0.9);
|
||||||
|
let pixel_size = u.glyph.x * mix(0.72, 1.85, shaped);
|
||||||
|
let ndc_offset = vertex.local * vec2f(pixel_size / u.viewport.x, pixel_size / u.viewport.y) * 2.0;
|
||||||
|
var out: DotVertexOutput;
|
||||||
|
out.clip_position = vec4f(center.xy + ndc_offset * center.w, center.z, center.w);
|
||||||
|
out.local = vertex.local;
|
||||||
|
out.intensity = intensity;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_dot(in: DotVertexOutput) -> @location(0) vec4f {
|
||||||
|
let intensity = saturate(in.intensity);
|
||||||
|
let base_color = sample_range_color(intensity);
|
||||||
|
|
||||||
|
let core = circle_alpha(in.local, 0.42, 0.055);
|
||||||
|
|
||||||
|
let glow = circle_alpha(in.local, 0.78, 0.18) * intensity * 0.45;
|
||||||
|
|
||||||
|
let highlight_pos = in.local - vec2f(-0.18, 0.20);
|
||||||
|
|
||||||
|
let highlight = circle_alpha(highlight_pos, 0.16, 0.08) * 0.45;
|
||||||
|
|
||||||
|
let brightness = mix(0.82, 1.22, intensity);
|
||||||
|
|
||||||
|
let color = base_color * brightness + vec3f(1.0, 1.0, 1.0) * highlight;
|
||||||
|
|
||||||
|
let alpha = max(core, glow);
|
||||||
|
|
||||||
|
return vec4f(color, alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// model
|
||||||
|
struct ModelVertexInput {
|
||||||
|
@location(0) position: vec3f,
|
||||||
|
@location(1) tex_coords: vec2f,
|
||||||
|
@location(2) normal: vec3f,
|
||||||
|
@location(5) model_0: vec4f,
|
||||||
|
@location(6) model_1: vec4f,
|
||||||
|
@location(7) model_2: vec4f,
|
||||||
|
@location(8) model_3: vec4f,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ModelVertexOutput {
|
||||||
|
@builtin(position) clip_position: vec4f,
|
||||||
|
@location(0) world_normal: vec3f,
|
||||||
|
@location(1) world_position: vec3f,
|
||||||
|
@location(2) tex_coords: vec2f,
|
||||||
|
}
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vs_model(vertex: ModelVertexInput) -> ModelVertexOutput {
|
||||||
|
let model = mat4x4f(vertex.model_0, vertex.model_1, vertex.model_2, vertex.model_3);
|
||||||
|
let world_position = model * vec4f(vertex.position, 1.0);
|
||||||
|
let normal_matrix = mat3x3f(model[0].xyz, model[1].xyz, model[2].xyz);
|
||||||
|
|
||||||
|
var out: ModelVertexOutput;
|
||||||
|
out.clip_position = u.view_proj * world_position;
|
||||||
|
out.world_position = world_position.xyz;
|
||||||
|
out.world_normal = normalize(normal_matrix * vertex.normal);
|
||||||
|
out.tex_coords = vec2f(vertex.tex_coords.x, 1.0 - vertex.tex_coords.y);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_model(in: ModelVertexOutput) -> @location(0) vec4f {
|
||||||
|
let light_dir = normalize(vec3f(-0.35, 0.85, 0.45));
|
||||||
|
let normal = normalize(in.world_normal);
|
||||||
|
let diffuse = max(dot(normal, light_dir), 0.0);
|
||||||
|
let rim = pow(1.0 - saturate(abs(normal.y)), 2.0) * 0.16;
|
||||||
|
let base = textureSample(model_texture, model_sampler, in.tex_coords).rgb;
|
||||||
|
let color = base * (0.28 + diffuse * 0.72) + vec3f(0.14, 0.24, 0.32) * rim;
|
||||||
|
return vec4f(color, 1.0);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user