867 lines
18 KiB
Markdown
867 lines
18 KiB
Markdown
# 渲染流程梳理
|
||
|
||
这份文档拆解当前项目里的渲染链路,包括:
|
||
|
||
- 压力数据如何从 CPU 进入 GPU
|
||
- `egui_wgpu::CallbackTrait` 的 `prepare / paint` 两阶段
|
||
- `BackgroundRenderResources` 负责管理哪些 GPU 资源
|
||
- `shader.wgsl` 里的各个 shader entry point 分别做什么
|
||
- `Finger / Hand` 两种模式如何切换渲染路径
|
||
|
||
主要相关文件:
|
||
|
||
- `src/app.rs`
|
||
- `src/render.rs`
|
||
- `src/matrix.rs`
|
||
- `src/model.rs`
|
||
- `static/wgsl/shader.wgsl`
|
||
|
||
## 总览
|
||
|
||
渲染入口在 `EskinDesktopApp::ui`:
|
||
|
||
```rust
|
||
self.draw_wgpu_background(ui);
|
||
self.draw_panel_context_menu(ui);
|
||
self.draw_title_bar(ui, frame);
|
||
self.draw_floating_panels(&ctx);
|
||
```
|
||
|
||
顺序很重要:
|
||
|
||
1. `draw_wgpu_background` 先绘制整块 WGPU 背景和 3D 内容。
|
||
2. 右键菜单、标题栏、floating panel 后画,所以会盖在 WGPU 内容上。
|
||
3. `ctx.request_repaint()` 保证背景持续刷新。
|
||
|
||
真正的 WGPU 绘制不是直接在 `app.rs` 里执行,而是通过 egui 的 paint callback:
|
||
|
||
```rust
|
||
ui.painter().add(egui_wgpu::Callback::new_paint_callback(
|
||
rect,
|
||
WgpuBackgroundCallback {
|
||
width,
|
||
height,
|
||
pressure: self.pressure_matrix,
|
||
active_mode: self.active_mode.clone(),
|
||
},
|
||
));
|
||
```
|
||
|
||
可以理解成:`app.rs` 每帧把“当前要画什么”打包成 `WgpuBackgroundCallback`,然后交给 `egui_wgpu` 在合适的 GPU 阶段执行。
|
||
|
||
## 数据流
|
||
|
||
### 1. 串口数据进入 App
|
||
|
||
`update_pressure_matrix` 从 `ConnectionManager` 取最新数据:
|
||
|
||
```rust
|
||
if let Some(sample) = self.connection.take_latest_sample() {
|
||
normalize_pressure_sample(
|
||
&sample.matrix,
|
||
sample.rows,
|
||
sample.cols,
|
||
&mut self.pressure_matrix,
|
||
);
|
||
}
|
||
```
|
||
|
||
串口原始数据是一个扁平的一维矩阵,元素是整数压力值。
|
||
|
||
`normalize_pressure_sample` 会把它转成:
|
||
|
||
```rust
|
||
pub type PressureFrame = [[f32; 2]; PRESSURE_CELL_COUNT];
|
||
```
|
||
|
||
每个 cell 有两个 `f32`:
|
||
|
||
```text
|
||
[normalized_pressure, display_value]
|
||
```
|
||
|
||
含义:
|
||
|
||
| 值 | 作用 |
|
||
| --- | --- |
|
||
| `normalized_pressure` | `0.0..=1.0`,用于颜色、大小、亮度。 |
|
||
| `display_value` | 原始压力值的显示版本,用于数字模式。 |
|
||
|
||
当前归一化范围写死在 `app.rs`:
|
||
|
||
```rust
|
||
const RANGE_MIN: f32 = 0.0;
|
||
const RANGE_MAX: f32 = 7000.0;
|
||
```
|
||
|
||
这意味着虽然 `FingerMode / HandGatewayMode` 里已经有 `range` 字段,但实际颜色归一化还没有用到它。
|
||
|
||
### 2. App 创建 WGPU Callback
|
||
|
||
`draw_wgpu_background` 根据当前 viewport 创建 callback:
|
||
|
||
```rust
|
||
WgpuBackgroundCallback {
|
||
width,
|
||
height,
|
||
pressure: self.pressure_matrix,
|
||
active_mode: self.active_mode.clone(),
|
||
}
|
||
```
|
||
|
||
`WgpuBackgroundCallback` 保存的是“每帧变化的数据”:
|
||
|
||
- 当前视口宽度
|
||
- 当前视口高度
|
||
- 当前压力帧
|
||
- 当前渲染模式
|
||
|
||
它不保存长期 GPU 资源。长期资源在 `BackgroundRenderResources` 里。
|
||
|
||
### 3. Callback Prepare 阶段
|
||
|
||
`WgpuBackgroundCallback` 实现了:
|
||
|
||
```rust
|
||
impl egui_wgpu::CallbackTrait for WgpuBackgroundCallback
|
||
```
|
||
|
||
`prepare` 阶段代码:
|
||
|
||
```rust
|
||
let resources: &mut BackgroundRenderResources = resources.get_mut().unwrap();
|
||
resources.prepare(queue, self.width, self.height, &self.pressure);
|
||
```
|
||
|
||
这个阶段主要做 CPU -> GPU 的数据更新:
|
||
|
||
1. 根据 viewport aspect ratio 重新计算相机矩阵。
|
||
2. 把 `MatrixUniform` 写入 `uniform_buffer`。
|
||
3. 根据当前 `PressureFrame` 更新所有 marker instance。
|
||
4. 把 `glyph_instances` 写入 `glyph_instance_buffer`。
|
||
|
||
这里不会真正 draw,只是准备 GPU buffer。
|
||
|
||
### 4. Callback Paint 阶段
|
||
|
||
`paint` 阶段代码:
|
||
|
||
```rust
|
||
let resources: &BackgroundRenderResources = resources.get().unwrap();
|
||
resources.paint(render_pass, &self.active_mode);
|
||
```
|
||
|
||
这个阶段才是真正向 `render_pass` 发 draw call。
|
||
|
||
`active_mode` 在这里传进去,决定后面画点阵、数字,还是画模型。
|
||
|
||
## GPU 资源管理
|
||
|
||
`BackgroundRenderResources` 在 `EskinDesktopApp::new` 里创建一次:
|
||
|
||
```rust
|
||
renderer
|
||
.callback_resources
|
||
.insert(BackgroundRenderResources::new(
|
||
&wgpu_state.device,
|
||
&wgpu_state.queue,
|
||
&wgpu_state.target_format,
|
||
MATRIX_ROWS,
|
||
MATRIX_COLS,
|
||
));
|
||
```
|
||
|
||
它负责保存长期存在的 GPU 资源:
|
||
|
||
```rust
|
||
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,
|
||
dot_pipeline: wgpu::RenderPipeline,
|
||
model_pipeline: wgpu::RenderPipeline,
|
||
model: Option<Model>,
|
||
model_instance_buffer: wgpu::Buffer,
|
||
glyph_vertex_buffer: wgpu::Buffer,
|
||
glyph_instance_buffer: wgpu::Buffer,
|
||
glyph_instances: Vec<GlyphInstance>,
|
||
}
|
||
```
|
||
|
||
字段含义:
|
||
|
||
| 字段 | 作用 |
|
||
| --- | --- |
|
||
| `layout` | 压力矩阵的世界空间布局。 |
|
||
| `rows`, `cols` | 当前 GPU 资源按多少行列创建。 |
|
||
| `uniform` | CPU 侧 uniform 缓存。 |
|
||
| `uniform_buffer` | GPU uniform buffer。 |
|
||
| `uniform_bind_group` | WGSL 里的 `@group(0)`。 |
|
||
| `background_pipeline` | 全屏背景、底部色条。 |
|
||
| `glyph_pipeline` | 数字/数值 marker。 |
|
||
| `dot_pipeline` | 圆点 marker。 |
|
||
| `model_pipeline` | OBJ 模型渲染。 |
|
||
| `model` | 加载好的模型和材质。 |
|
||
| `model_instance_buffer` | 模型实例矩阵。 |
|
||
| `glyph_vertex_buffer` | 一个 marker quad 的 6 个顶点。 |
|
||
| `glyph_instance_buffer` | 每个压力点一个 instance。 |
|
||
| `glyph_instances` | CPU 侧 instance 数组。 |
|
||
|
||
## 模式切换
|
||
|
||
当前运行时渲染模式:
|
||
|
||
```rust
|
||
pub enum ActiveMode {
|
||
Finger(FingerMode),
|
||
Hand(HandGatewayMode),
|
||
}
|
||
```
|
||
|
||
`app.rs` 持有:
|
||
|
||
```rust
|
||
active_mode: ActiveMode,
|
||
```
|
||
|
||
UI 层的 `SerialMode` 是用户选择,真正渲染用的是 `ActiveMode`。
|
||
|
||
切换逻辑在 `switch_mode`:
|
||
|
||
```rust
|
||
fn switch_mode(&mut self, next: SerialMode) {
|
||
self.connect_state.mode = next;
|
||
self.config_state.mode = next;
|
||
self.connection.disconnect();
|
||
self.pressure_matrix.fill([0.0, 0.0]);
|
||
self.data_log_frame = 0;
|
||
|
||
self.active_mode = match next {
|
||
SerialMode::Finger => ActiveMode::Finger(FingerMode {
|
||
rows: self.matrix_config.rows,
|
||
cols: self.matrix_config.cols,
|
||
range: 0..7000,
|
||
dot: true,
|
||
}),
|
||
SerialMode::Hand => ActiveMode::Hand(HandGatewayMode { range: 0..7000 }),
|
||
}
|
||
}
|
||
```
|
||
|
||
这里做了几件事:
|
||
|
||
1. 同步 UI 状态。
|
||
2. 断开当前连接。
|
||
3. 清空压力帧。
|
||
4. 重置日志计数。
|
||
5. 设置新的 `active_mode`。
|
||
|
||
这比只改一个 enum 更像真正的互斥生命周期切换。
|
||
|
||
## 绘制分发
|
||
|
||
`BackgroundRenderResources::paint` 先画背景,再按模式分支:
|
||
|
||
```rust
|
||
render_pass.set_pipeline(&self.background_pipeline);
|
||
render_pass.draw(0..3, 0..1);
|
||
|
||
match active_mode {
|
||
ActiveMode::Finger(mode) => self.paint_finger(render_pass, mode),
|
||
ActiveMode::Hand(mode) => self.paint_hand(render_pass, mode),
|
||
}
|
||
```
|
||
|
||
当前模式行为:
|
||
|
||
| 模式 | 绘制内容 |
|
||
| --- | --- |
|
||
| `Finger` | 背景 + 压力 marker。 |
|
||
| `Hand` | 背景 + 模型。 |
|
||
|
||
## Finger 模式
|
||
|
||
Finger 模式根据 `dot` 字段选择点模式或数字模式:
|
||
|
||
```rust
|
||
let marker_pipeline = if mode.dot {
|
||
&self.dot_pipeline
|
||
} else {
|
||
&self.glyph_pipeline
|
||
};
|
||
```
|
||
|
||
含义:
|
||
|
||
| `mode.dot` | pipeline | 效果 |
|
||
| --- | --- | --- |
|
||
| `true` | `dot_pipeline` | 画圆点。 |
|
||
| `false` | `glyph_pipeline` | 画数字。 |
|
||
|
||
两个 pipeline 共用同一套 vertex buffer:
|
||
|
||
```rust
|
||
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..draw_count);
|
||
```
|
||
|
||
解释:
|
||
|
||
- vertex buffer 0:一个 quad 的 6 个顶点。
|
||
- vertex buffer 1:每个压力点一个 instance。
|
||
- `draw(0..6, 0..draw_count)`:每个 instance 都复用这 6 个顶点。
|
||
|
||
## Hand 模式
|
||
|
||
Hand 模式当前只画模型:
|
||
|
||
```rust
|
||
render_pass.set_pipeline(&self.model_pipeline);
|
||
render_pass.set_vertex_buffer(1, self.model_instance_buffer.slice(..));
|
||
```
|
||
|
||
然后遍历 mesh:
|
||
|
||
```rust
|
||
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);
|
||
}
|
||
```
|
||
|
||
绑定关系:
|
||
|
||
| Slot / Group | 内容 |
|
||
| --- | --- |
|
||
| bind group 0 | `MatrixUniform` |
|
||
| bind group 1 | 模型纹理和 sampler |
|
||
| vertex buffer 0 | 模型顶点 |
|
||
| vertex buffer 1 | 模型实例矩阵 |
|
||
|
||
## Matrix 布局
|
||
|
||
`src/matrix.rs` 定义压力矩阵如何放到 3D 世界里。
|
||
|
||
固定默认尺寸:
|
||
|
||
```rust
|
||
pub const MATRIX_ROWS: u32 = 12;
|
||
pub const MATRIX_COLS: u32 = 7;
|
||
```
|
||
|
||
`MatrixLayout::new(rows, cols)` 计算:
|
||
|
||
- cell 间距
|
||
- board 宽度
|
||
- board 深度
|
||
- board padding
|
||
- marker 垂直浮起高度
|
||
|
||
`glyph_world_position` 把 `(row, col)` 映射到世界坐标:
|
||
|
||
```rust
|
||
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;
|
||
```
|
||
|
||
返回:
|
||
|
||
```rust
|
||
[
|
||
x,
|
||
MATRIX_OFFSET_Y + layout.label_float_offset,
|
||
MATRIX_OFFSET_Z + z,
|
||
1.0,
|
||
]
|
||
```
|
||
|
||
所以 marker 是铺在 X/Z 平面上,Y 方向稍微浮起来。
|
||
|
||
`build_view_projection` 根据矩阵尺寸创建相机:
|
||
|
||
```rust
|
||
let view = glam::Mat4::look_at_rh(eye, target, glam::Vec3::Y);
|
||
let projection = glam::Mat4::perspective_rh(...);
|
||
let open_gl_to_wgpu = ...
|
||
```
|
||
|
||
最后的 `open_gl_to_wgpu` 是为了把 OpenGL 风格深度范围转换成 WGPU 需要的 clip space。
|
||
|
||
## Uniform
|
||
|
||
Rust 侧:
|
||
|
||
```rust
|
||
#[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],
|
||
}
|
||
```
|
||
|
||
WGSL 侧:
|
||
|
||
```wgsl
|
||
struct MatrixUniform {
|
||
view_proj: mat4x4f,
|
||
viewport: vec4f,
|
||
glyph: vec4f,
|
||
color: vec4f,
|
||
}
|
||
|
||
@group(0) @binding(0)
|
||
var<uniform> u: MatrixUniform;
|
||
```
|
||
|
||
字段含义:
|
||
|
||
| 字段 | 含义 |
|
||
| --- | --- |
|
||
| `view_proj` | 世界坐标到 clip space 的矩阵。 |
|
||
| `viewport.xy` | 当前 viewport 像素尺寸。 |
|
||
| `glyph.x` | marker 基础像素大小。 |
|
||
| `color` | 目前基本没用上,预留字段。 |
|
||
|
||
每帧在 `prepare` 里写入 GPU:
|
||
|
||
```rust
|
||
queue.write_buffer(
|
||
&self.uniform_buffer,
|
||
0,
|
||
bytemuck::cast_slice(&[self.uniform]),
|
||
);
|
||
```
|
||
|
||
## Marker / Glyph 几何
|
||
|
||
代码里叫 `glyph`,但它不是字体系统里的 glyph。它更像一个“压力 marker”。
|
||
|
||
基础几何是一个 quad:
|
||
|
||
```rust
|
||
let glyph_vertices = [
|
||
[-1.0, -1.0],
|
||
[ 1.0, -1.0],
|
||
[-1.0, 1.0],
|
||
[-1.0, 1.0],
|
||
[ 1.0, -1.0],
|
||
[ 1.0, 1.0],
|
||
];
|
||
```
|
||
|
||
这 6 个点组成两个三角形。
|
||
|
||
每个压力点对应一个 `GlyphInstance`:
|
||
|
||
```rust
|
||
struct GlyphInstance {
|
||
world_position: [f32; 4],
|
||
style: [f32; 4],
|
||
}
|
||
```
|
||
|
||
WGSL 接收:
|
||
|
||
```wgsl
|
||
struct GlyphInstanceInput {
|
||
@location(1) world_position: vec4f,
|
||
@location(2) style: vec4f,
|
||
}
|
||
```
|
||
|
||
`style` 的当前含义:
|
||
|
||
| 分量 | 含义 |
|
||
| --- | --- |
|
||
| `style.x` | 归一化压力值。 |
|
||
| `style.y` | 显示用压力值。 |
|
||
| `style.z` | 预留。 |
|
||
| `style.w` | 预留。 |
|
||
|
||
## Shader 总览
|
||
|
||
所有 pipeline 当前共用一个 WGSL 文件:
|
||
|
||
```rust
|
||
include_str!("../static/wgsl/shader.wgsl")
|
||
```
|
||
|
||
不同 pipeline 选择不同入口函数。
|
||
|
||
## Background Shader
|
||
|
||
Rust 侧创建:
|
||
|
||
```rust
|
||
create_background_pipeline(...)
|
||
```
|
||
|
||
WGSL entry point:
|
||
|
||
```wgsl
|
||
@vertex
|
||
fn vs_background(...)
|
||
|
||
@fragment
|
||
fn fs_background(...)
|
||
```
|
||
|
||
`vs_background` 画一个全屏三角形:
|
||
|
||
```wgsl
|
||
let positions = array<vec2f, 3>(
|
||
vec2f(-1.0, -3.0),
|
||
vec2f(3.0, 1.0),
|
||
vec2f(-1.0, 1.0),
|
||
);
|
||
```
|
||
|
||
一个大三角形覆盖整屏,避免两个三角形拼接时可能出现的边界问题。
|
||
|
||
`fs_background` 负责画:
|
||
|
||
- 深色垂直渐变
|
||
- vignette 暗角
|
||
- 中央氛围带
|
||
- 底部压力色条
|
||
- 色条刻度线
|
||
|
||
颜色映射来自:
|
||
|
||
```wgsl
|
||
fn sample_range_color(value: f32) -> vec3f
|
||
```
|
||
|
||
色阶大概是:
|
||
|
||
1. 蓝青
|
||
2. 绿色
|
||
3. 橙色
|
||
4. 红色
|
||
|
||
## Glyph / 数字 Shader
|
||
|
||
Rust 侧创建:
|
||
|
||
```rust
|
||
create_glyph_pipeline(...)
|
||
```
|
||
|
||
WGSL entry point:
|
||
|
||
```wgsl
|
||
@vertex
|
||
fn vs_glyph(...)
|
||
|
||
@fragment
|
||
fn fs_glyph(...)
|
||
```
|
||
|
||
这是数字模式。
|
||
|
||
`vs_glyph` 做几件事:
|
||
|
||
1. 把 instance 的 `world_position` 乘 `u.view_proj`,得到 clip space 中心点。
|
||
2. 从 `instance.style.x` 读取压力强度。
|
||
3. 根据强度计算 marker 的像素大小。
|
||
4. 用 `viewport` 把像素大小转换成 NDC 偏移。
|
||
|
||
关键代码:
|
||
|
||
```wgsl
|
||
let shaped = pow(saturate(instance.style.x), 0.9);
|
||
let pixel_size = u.glyph.x * mix(1.08, 2.20, shaped);
|
||
let ndc_offset = vertex.local * vec2f(pixel_size / u.viewport.x, pixel_size / u.viewport.y) * 2.0;
|
||
```
|
||
|
||
这意味着数字不是固定世界尺寸,而是接近固定屏幕像素尺寸,更适合读数。
|
||
|
||
`fs_glyph` 做数字绘制:
|
||
|
||
```wgsl
|
||
let alpha = number_alpha(in.local, in.display_value);
|
||
let color = sample_range_color(in.intensity) * mix(0.82, 1.16, saturate(in.intensity));
|
||
return vec4f(color, alpha);
|
||
```
|
||
|
||
数字不是字体贴图,而是在 shader 里用七段数码管方式画出来。
|
||
|
||
相关函数:
|
||
|
||
- `digit_segment_on`
|
||
- `seven_segment_digit_alpha`
|
||
- `digit_count`
|
||
- `digit_at`
|
||
- `number_alpha`
|
||
|
||
## Dot / 点 Shader
|
||
|
||
Rust 侧创建:
|
||
|
||
```rust
|
||
create_dot_pipeline(...)
|
||
```
|
||
|
||
WGSL entry point:
|
||
|
||
```wgsl
|
||
@vertex
|
||
fn vs_dot(...)
|
||
|
||
@fragment
|
||
fn fs_dot(...)
|
||
```
|
||
|
||
这是圆点模式。
|
||
|
||
`vs_dot` 和 `vs_glyph` 类似,也是把世界坐标点展开成屏幕像素大小的 quad。
|
||
|
||
点模式的尺寸曲线更小:
|
||
|
||
```wgsl
|
||
let pixel_size = u.glyph.x * mix(0.72, 1.85, shaped);
|
||
```
|
||
|
||
`fs_dot` 画:
|
||
|
||
- 圆形核心
|
||
- 外发光
|
||
- 小高光
|
||
- 压力映射颜色
|
||
|
||
圆形 alpha 来自:
|
||
|
||
```wgsl
|
||
fn circle_alpha(local: vec2f, radius: f32, softness: f32) -> f32
|
||
```
|
||
|
||
核心片段:
|
||
|
||
```wgsl
|
||
let core = circle_alpha(in.local, 0.42, 0.055);
|
||
let glow = circle_alpha(in.local, 0.78, 0.18) * intensity * 0.45;
|
||
let alpha = max(core, glow);
|
||
```
|
||
|
||
## Model Shader
|
||
|
||
Rust 侧创建:
|
||
|
||
```rust
|
||
create_model_pipeline(...)
|
||
```
|
||
|
||
WGSL entry point:
|
||
|
||
```wgsl
|
||
@vertex
|
||
fn vs_model(...)
|
||
|
||
@fragment
|
||
fn fs_model(...)
|
||
```
|
||
|
||
输入 layout:
|
||
|
||
```wgsl
|
||
@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,
|
||
```
|
||
|
||
`position / tex_coords / normal` 来自模型 vertex buffer。
|
||
|
||
`model_0..model_3` 来自 instance buffer,也就是每个模型实例的 transform matrix。
|
||
|
||
`vs_model`:
|
||
|
||
1. 重建 model matrix。
|
||
2. 计算 world position。
|
||
3. 用 `u.view_proj` 转到 clip space。
|
||
4. 变换 normal。
|
||
5. 翻转纹理 Y 坐标。
|
||
|
||
`fs_model`:
|
||
|
||
1. 采样纹理。
|
||
2. 计算简单漫反射光照。
|
||
3. 加一点 rim light。
|
||
4. 输出不透明颜色。
|
||
|
||
```wgsl
|
||
let light_dir = normalize(vec3f(-0.35, 0.85, 0.45));
|
||
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;
|
||
```
|
||
|
||
## 模型加载
|
||
|
||
模型在 `BackgroundRenderResources::new` 里加载一次:
|
||
|
||
```rust
|
||
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
|
||
}
|
||
};
|
||
```
|
||
|
||
当前模型实例 transform 是写死的:
|
||
|
||
```rust
|
||
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();
|
||
```
|
||
|
||
它把模型放到和压力矩阵相同的相机空间里。
|
||
|
||
## 当前限制
|
||
|
||
### 1. `rows / cols` 还没有真正重建布局
|
||
|
||
`FingerMode` 里有:
|
||
|
||
```rust
|
||
rows: u32,
|
||
cols: u32,
|
||
```
|
||
|
||
但 `BackgroundRenderResources` 的 `layout` 和 instance buffer 是创建时确定的。
|
||
|
||
现在 `paint_finger` 只用 rows/cols 限制 draw count:
|
||
|
||
```rust
|
||
let draw_count = self.visible_instance_count(mode.rows, mode.cols);
|
||
```
|
||
|
||
所以它能少画一些点,但不会重新计算新尺寸下的空间布局。
|
||
|
||
如果后面要支持动态矩阵尺寸,需要:
|
||
|
||
1. 尺寸变化时重建 `MatrixLayout` 和 instance buffer。
|
||
2. 或者一开始分配最大尺寸 buffer,然后每帧按 active rows/cols 更新坐标。
|
||
|
||
### 2. `range` 字段还没有真正用起来
|
||
|
||
`FingerMode` 和 `HandGatewayMode` 都有:
|
||
|
||
```rust
|
||
range: Range<u32>
|
||
```
|
||
|
||
但现在归一化仍在 `normalize_pressure_sample` 里硬编码为 `0..7000`。
|
||
|
||
所以如果要让不同模式有不同色域范围,需要把 range 传入归一化过程,或者把归一化移动到 render/update instance 阶段。
|
||
|
||
### 3. `glyph` 命名有点误导
|
||
|
||
现在的 `glyph` 实际更像 marker。
|
||
|
||
推荐未来改名:
|
||
|
||
```text
|
||
GlyphVertex -> MarkerVertex
|
||
GlyphInstance -> MarkerInstance
|
||
glyph_pipeline -> number_marker_pipeline
|
||
dot_pipeline -> dot_marker_pipeline
|
||
glyph_vertex_buffer -> marker_vertex_buffer
|
||
glyph_instance_buffer -> marker_instance_buffer
|
||
glyph_instances -> marker_instances
|
||
```
|
||
|
||
### 4. Hand 模式还没有压力叠加
|
||
|
||
现在:
|
||
|
||
```rust
|
||
ActiveMode::Hand(mode) => self.paint_hand(render_pass, mode),
|
||
```
|
||
|
||
只画模型,不画压力点。
|
||
|
||
如果手掌模式要显示压力映射,需要额外设计“压力矩阵 -> 手模型表面/区域”的映射。
|
||
|
||
可能方向:
|
||
|
||
```rust
|
||
ActiveMode::Hand(mode) => {
|
||
self.paint_hand(render_pass, mode);
|
||
self.paint_hand_pressure_overlay(render_pass, mode);
|
||
}
|
||
```
|
||
|
||
但 overlay 的坐标不应该复用现在的矩阵 X/Z 网格,需要新的手部映射数据。
|
||
|
||
## 建议下一步
|
||
|
||
把:
|
||
|
||
```rust
|
||
pub struct FingerMode {
|
||
pub rows: u32,
|
||
pub cols: u32,
|
||
pub range: Range<u32>,
|
||
pub dot: bool,
|
||
}
|
||
```
|
||
|
||
改成:
|
||
|
||
```rust
|
||
pub enum MarkerMode {
|
||
Dot,
|
||
Number,
|
||
}
|
||
|
||
pub struct FingerMode {
|
||
pub rows: u32,
|
||
pub cols: u32,
|
||
pub range: Range<u32>,
|
||
pub marker_mode: MarkerMode,
|
||
}
|
||
```
|
||
|
||
这样比 `dot: bool` 更清楚。
|
||
|
||
现在的:
|
||
|
||
```rust
|
||
dot: true
|
||
```
|
||
|
||
需要脑内翻译成“点模式”。
|
||
|
||
改成:
|
||
|
||
```rust
|
||
marker_mode: MarkerMode::Dot
|
||
```
|
||
|
||
代码语义会直接很多。
|
||
|