2025-12-18 09:19:39 +08:00
2025-12-18 09:02:33 +08:00
2025-12-18 09:19:39 +08:00
2025-12-18 09:19:39 +08:00
2025-12-18 09:19:39 +08:00
2025-12-18 09:19:39 +08:00
2025-12-18 09:19:39 +08:00
2025-12-18 09:19:39 +08:00
2025-12-18 09:19:39 +08:00
2025-12-18 09:19:39 +08:00
2025-12-18 09:19:39 +08:00
2025-12-18 09:19:39 +08:00
2025-12-18 09:19:39 +08:00

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

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]

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,vUV 坐标fragment shader 用它来判断像素是否在圆内(圆外就 discard

然后绑定 attribute

// 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 的声明对应:

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

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

// 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 里的声明:

layout(location = 2) in vec2 iOffsetXZ;
layout(location = 3) in float iValue;

3) 每帧如何更新 dots 的位置和值(上传 instance buffer

GLWidget::updateInstanceBufferIfNeeded_() 里,代码会把点阵居中摆放,并把 value 写进去:

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

glBindBuffer(GL_ARRAY_BUFFER, m_instanceVbo);
glBufferSubData(GL_ARRAY_BUFFER, 0, inst.size() * sizeof(float), inst.constData());

4) Vertex Shader把 quad “放到每个点的位置”

shaders/dots.vert(简化后的核心逻辑):

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(核心逻辑):

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 的部分大概是:

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();

uBaseY0.001f 是为了避免 dots 和 panel 顶面完全重合产生 z-fighting闪烁


常见“看不到点”的原因(排查清单)

  1. vertex shader 没写 gl_Position(会直接编译失败/链接失败)
  2. shader 文件没读到Qt Resource 一定要用 :/shaders/xxx.vert 这种路径)
  3. m_instanceCount == 0dotCount()==0rows/cols 没设置)
  4. 忘了 glVertexAttribDivisor(2/3, 1)instance 数据会“按顶点乱跳”)
  5. uniform 类型不匹配(例如 shader 里是 floatC++ 却用 int 设置)
  6. 相机没对准uMVP 不正确/相机离得太近或太远)

想继续改进?

  • 想让 dot 大小随 value 变化:在 dots.vert 里用 iValueuDotRadius(比如乘一个比例)
  • 想要更柔和的圆边:用 smoothstep 做 alpha 边缘 + 开启 blending
  • 想要真正的 3D 圆柱/球:就不能靠 discard 了,需要真正的几何或法线光照
Description
No description provided
Readme 173 KiB
Languages
C 82.1%
C++ 12.1%
GLSL 3.6%
QML 1.6%
CMake 0.6%