Files
tactileipc3d/README.md
2025-12-18 09:19:39 +08:00

260 lines
7.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# TactileIpc3D圆点传感点绘制说明
这份 README 专门把“圆点dots是怎么画出来的”单独拎出来讲清楚**CPU 端准备数据**、到 **GPU 端着色器如何把一个 quad 变成很多个圆点**
---
## 关键文件(只看这几个就够)
- C++ 侧
- `src/glwidget.cpp`
- `GLWidget::initDotGeometry_()`:创建 dots 的 VAO/VBO/instanceVBO并声明 attribute 布局
- `GLWidget::updateInstanceBufferIfNeeded_()`:把每个点的 `(offsetX, offsetZ, value)` 上传到 GPU
- `GLWidget::paintGL()`:设置 uniforms然后 `glDrawArraysInstanced()` 一次性画出 N 个点
- GLSL 侧shader
- `shaders/dots.vert`:把“单位 quad”放到每个点的位置并乘上 `uMVP`
- `shaders/dots.frag`:在像素级别把 quad “裁剪”为圆形,并根据 value 上色
---
## 总体思路:一个 quad + instanced rendering
我们并没有为每个点生成一个圆形网格(那样会很重),而是:
1. **只建一个单位 quad两个三角形**,共 6 个顶点。
2.**Instanced Rendering**:一条 draw call 画很多个 instance。
3. 每个 instance 在 GPU 上拿到自己的 **位置偏移** + **传感值**,从而变成不同位置/不同颜色的圆点。
对应的 OpenGL draw call
```cpp
glDrawArraysInstanced(GL_TRIANGLES, 0, 6, m_instanceCount);
```
- `0..6`:每个点用 6 个顶点(两个三角形)组成 quad
- `m_instanceCount`要画多少个点rows*cols
---
## 1) Dot 的“基础几何”:单位 quadVBO, per-vertex
`GLWidget::initDotGeometry_()` 里,先创建一个单位 quad。它在局部空间的范围是 `[-1, 1]`
```cpp
struct V { float x, y; float u, v; };
V quad[6] = {
{-1,-1, 0,0}, { 1,-1, 1,0}, { 1, 1, 1,1},
{-1,-1, 0,0}, { 1, 1, 1,1}, {-1, 1, 0,1},
};
```
这里的含义:
- `x,y`:这个 quad 的“局部顶点坐标”,后续会乘以 `uDotRadius` 变成实际大小
- `u,v`UV 坐标fragment shader 用它来判断像素是否在圆内(圆外就 `discard`
然后绑定 attribute
```cpp
// location 0: quad 顶点位置 (x,y)
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(V), (void*)0);
// location 1: UV (u,v)
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(V), (void*)(2 * sizeof(float)));
```
这必须和 `shaders/dots.vert` 的声明对应:
```glsl
layout(location = 0) in vec2 qQuadPos;
layout(location = 1) in vec2 aUV;
```
---
## 2) Instance 数据每个点一条记录instanceVBO, per-instance
每个点instance需要独有的数据。我们用 3 个 float 表示:
- `offsetX`
- `offsetZ`
- `value`(传感值,用于上色)
在 C++ 里分配 instance VBO
```cpp
glGenBuffers(1, &m_instanceVbo);
glBindBuffer(GL_ARRAY_BUFFER, m_instanceVbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 3 * qMax(1, dotCount()), nullptr, GL_DYNAMIC_DRAW);
```
然后把它绑定到 attribute location 2/3并设置 divisor=1
```cpp
// location 2: vec2 iOffsetXZ每个 instance 使用一次)
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glVertexAttribDivisor(2, 1);
// location 3: float iValue每个 instance 使用一次)
glEnableVertexAttribArray(3);
glVertexAttribPointer(3, 1, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)(2 * sizeof(float)));
glVertexAttribDivisor(3, 1);
```
`glVertexAttribDivisor(..., 1)` 是 instancing 的关键:
- divisor=0默认每个顶点取一条数据per-vertex
- divisor=1每画一个 instance 才更新一次per-instance
对应 shader 里的声明:
```glsl
layout(location = 2) in vec2 iOffsetXZ;
layout(location = 3) in float iValue;
```
---
## 3) 每帧如何更新 dots 的位置和值(上传 instance buffer
`GLWidget::updateInstanceBufferIfNeeded_()` 里,代码会把点阵居中摆放,并把 value 写进去:
```cpp
const float w = (m_cols - 1) * m_pitch;
const float h = (m_rows - 1) * m_pitch;
for (int i = 0; i < n; ++i) {
const int r = (m_cols > 0) ? (i / m_cols) : 0;
const int c = (m_cols > 0) ? (i % m_cols) : 0;
const float x = (c * m_pitch) - w * 0.5f;
const float z = (r * m_pitch) - h * 0.5f;
inst[i * 3 + 0] = x; // offsetX
inst[i * 3 + 1] = z; // offsetZ
inst[i * 3 + 2] = valuesCopy[i]; // value
}
```
最后把 `inst` 上传到 GPU
```cpp
glBindBuffer(GL_ARRAY_BUFFER, m_instanceVbo);
glBufferSubData(GL_ARRAY_BUFFER, 0, inst.size() * sizeof(float), inst.constData());
```
---
## 4) Vertex Shader把 quad “放到每个点的位置”
`shaders/dots.vert`(简化后的核心逻辑):
```glsl
layout(location = 0) in vec2 qQuadPos; // 单位 quad 顶点
layout(location = 1) in vec2 aUV; // UV
layout(location = 2) in vec2 iOffsetXZ; // 每个点的 offsetX,Z
layout(location = 3) in float iValue; // 每个点的 value
out vec2 vUV;
out float vValue;
uniform mat4 uMVP;
uniform float uDotRadius;
uniform float uBaseY;
void main() {
vUV = aUV;
vValue = iValue;
// dot 中心点(世界坐标)
vec3 world = vec3(iOffsetXZ.x, uBaseY, iOffsetXZ.y);
// 把单位 quad 按半径缩放,并铺在 XZ 平面上
world.x += qQuadPos.x * uDotRadius;
world.z += qQuadPos.y * uDotRadius;
// 世界坐标 -> 裁剪空间
gl_Position = uMVP * vec4(world, 1.0);
}
```
这里要理解的点:
- `qQuadPos` 是局部空间([-1,1]),乘 `uDotRadius` 才是实际大小
- `iOffsetXZ` 是每个点的平移(让同一个 quad 出现在不同位置)
- `uMVP` 把世界坐标变换到屏幕(不然你会“啥也看不到”)
---
## 5) Fragment Shader把 quad “裁剪成圆”,并用 value 上色
`shaders/dots.frag`(核心逻辑):
```glsl
in vec2 vUV;
in float vValue;
out vec4 FragColor;
uniform float uMinV;
uniform float uMaxV;
void main() {
// vUV: [0,1] -> [-1,1]
vec2 p = vUV * 2.0 - 1.0;
float r2 = dot(p, p);
if (r2 > 1.0) discard; // 圆外丢弃像素
float t = clamp((vValue - uMinV) / max(1e-6, (uMaxV - uMinV)), 0.0, 1.0);
vec3 heat = mix(vec3(0, 1, 0), vec3(1, 0, 0), t); // 绿->红
FragColor = vec4(heat, 1.0);
}
```
这样一个 quad 就“看起来像圆点”了。
---
## 6) 真正开始画:设置 uniforms + draw
`GLWidget::paintGL()` 里,绘制 dots 的部分大概是:
```cpp
m_dotsProg->bind();
m_dotsProg->setUniformValue("uMVP", m_mvp);
m_dotsProg->setUniformValue("uDotRadius", m_dotRadius);
m_dotsProg->setUniformValue("uBaseY", (m_panelH * 0.5f) + 0.001f);
m_dotsProg->setUniformValue("uMinV", float(m_min));
m_dotsProg->setUniformValue("uMaxV", float(m_max));
glBindVertexArray(m_dotsVao);
glDrawArraysInstanced(GL_TRIANGLES, 0, 6, m_instanceCount);
glBindVertexArray(0);
m_dotsProg->release();
```
> `uBaseY` 加 `0.001f` 是为了避免 dots 和 panel 顶面完全重合产生 z-fighting闪烁
---
## 常见“看不到点”的原因(排查清单)
1. **vertex shader 没写 `gl_Position`**(会直接编译失败/链接失败)
2. **shader 文件没读到**Qt Resource 一定要用 `:/shaders/xxx.vert` 这种路径)
3. **`m_instanceCount == 0`** 或 `dotCount()==0`rows/cols 没设置)
4. **忘了 `glVertexAttribDivisor(2/3, 1)`**instance 数据会“按顶点乱跳”)
5. **uniform 类型不匹配**(例如 shader 里是 `float`C++ 却用 `int` 设置)
6. **相机没对准**`uMVP` 不正确/相机离得太近或太远)
---
## 想继续改进?
- 想让 dot 大小随 value 变化:在 `dots.vert` 里用 `iValue``uDotRadius`(比如乘一个比例)
- 想要更柔和的圆边:用 `smoothstep` 做 alpha 边缘 + 开启 blending
- 想要真正的 3D 圆柱/球:就不能靠 discard 了,需要真正的几何或法线光照