v2.0.0
Loading...
Searching...
No Matches
electrodeobject.cpp
Go to the documentation of this file.
1//=============================================================================================================
12
13//=============================================================================================================
14// INCLUDES
15//=============================================================================================================
16
17#include "electrodeobject.h"
18
19#include <rhi/qrhi.h>
20
21#include <QtMath>
22
23#include <limits>
24
25//=============================================================================================================
26// USED NAMESPACES
27//=============================================================================================================
28
29using namespace DISP3DLIB;
30
31//=============================================================================================================
32// PIMPL
33//=============================================================================================================
34
36{
37 std::unique_ptr<QRhiBuffer> vertexBuffer; // shaft geometry (pos+normal)
38 std::unique_ptr<QRhiBuffer> indexBuffer; // shaft triangle indices
39 std::unique_ptr<QRhiBuffer> instanceBuffer; // per-contact instance data
40 uint32_t shaftIndexCount = 0;
41 uint32_t instanceCount = 0;
42 bool dirty = true;
43};
44
45//=============================================================================================================
46// DEFINE MEMBER METHODS
47//=============================================================================================================
48
50: m_bbMin(std::numeric_limits<float>::max(),
51 std::numeric_limits<float>::max(),
52 std::numeric_limits<float>::max())
53, m_bbMax(std::numeric_limits<float>::lowest(),
54 std::numeric_limits<float>::lowest(),
55 std::numeric_limits<float>::lowest())
56, m_gpu(std::make_unique<GpuBuffers>())
57{
58}
59
60//=============================================================================================================
61
63
64//=============================================================================================================
65
66void ElectrodeObject::setArrays(const QVector<ElectrodeArray>& arrays)
67{
68 m_arrays = arrays;
69 m_selectedContact.clear();
70 computeBoundingBox();
71 m_gpu->dirty = true;
72}
73
74//=============================================================================================================
75
76const QVector<ElectrodeArray>& ElectrodeObject::arrays() const
77{
78 return m_arrays;
79}
80
81//=============================================================================================================
82
84{
85 int count = 0;
86 for (const auto& shaft : m_arrays)
87 count += shaft.contacts.size();
88 return count;
89}
90
91//=============================================================================================================
92
93void ElectrodeObject::setContactValues(const QMap<QString, float>& values,
94 const QColor& minColor,
95 const QColor& maxColor)
96{
97 if (values.isEmpty())
98 return;
99
100 // Find global min/max across provided values
101 float minVal = std::numeric_limits<float>::max();
102 float maxVal = std::numeric_limits<float>::lowest();
103 for (auto it = values.cbegin(); it != values.cend(); ++it) {
104 if (it.value() < minVal)
105 minVal = it.value();
106 if (it.value() > maxVal)
107 maxVal = it.value();
108 }
109
110 // Apply to contacts
111 for (auto& shaft : m_arrays) {
112 for (auto& contact : shaft.contacts) {
113 auto it = values.find(contact.name);
114 if (it != values.end()) {
115 contact.value = it.value();
116 contact.color = interpolateColor(it.value(), minVal, maxVal,
117 minColor, maxColor);
118 }
119 }
120 }
121 m_gpu->dirty = true;
122}
123
124//=============================================================================================================
125
126void ElectrodeObject::selectContact(const QString& name)
127{
128 // Clear previous selection
130
131 // Set new selection
132 m_selectedContact = name;
133 for (auto& shaft : m_arrays) {
134 for (auto& contact : shaft.contacts) {
135 if (contact.name == name) {
136 contact.selected = true;
137 m_gpu->dirty = true;
138 return;
139 }
140 }
141 }
142
143 // Not found — clear name
144 m_selectedContact.clear();
145}
146
147//=============================================================================================================
148
150{
151 m_selectedContact.clear();
152 for (auto& shaft : m_arrays) {
153 for (auto& contact : shaft.contacts)
154 contact.selected = false;
155 }
156 m_gpu->dirty = true;
157}
158
159//=============================================================================================================
160
162{
163 return m_selectedContact;
164}
165
166//=============================================================================================================
167
168void ElectrodeObject::generateShaftGeometry(QVector<float>& vertices,
169 QVector<unsigned int>& indices,
170 int cylinderSides) const
171{
172 vertices.clear();
173 indices.clear();
174
175 if (cylinderSides < 3)
176 cylinderSides = 3;
177
178 for (const auto& shaft : m_arrays) {
179 // v2.3.0: Strip and Grid layouts render as instanced spheres only
180 // (no cylinder shaft). They are emitted via generateContactInstances.
181 if (shaft.layout != ElectrodeLayout::Depth)
182 continue;
183 if (shaft.contacts.size() < 2)
184 continue;
185
186 const QVector3D& tipPos = shaft.contacts.first().position;
187 const QVector3D& tailPos = shaft.contacts.last().position;
188 const QVector3D axis = tailPos - tipPos;
189 const float length = axis.length();
190 if (length < 1e-6f)
191 continue;
192
193 const QVector3D axisNorm = axis.normalized();
194
195 // Build a perpendicular basis
196 QVector3D perp;
197 if (qAbs(QVector3D::dotProduct(axisNorm, QVector3D(0, 1, 0))) < 0.99f)
198 perp = QVector3D::crossProduct(axisNorm, QVector3D(0, 1, 0)).normalized();
199 else
200 perp = QVector3D::crossProduct(axisNorm, QVector3D(1, 0, 0)).normalized();
201
202 const QVector3D biperp = QVector3D::crossProduct(axisNorm, perp).normalized();
203
204 const float r = shaft.shaftRadius;
205 const unsigned int baseIdx = static_cast<unsigned int>(vertices.size() / 6);
206
207 // Generate circle vertices at tip and tail
208 for (int ring = 0; ring < 2; ++ring) {
209 const QVector3D center = (ring == 0) ? tipPos : tailPos;
210 for (int i = 0; i < cylinderSides; ++i) {
211 const float angle = 2.0f * float(M_PI) * float(i) / float(cylinderSides);
212 const float cs = cosf(angle);
213 const float sn = sinf(angle);
214
215 const QVector3D normal = (perp * cs + biperp * sn).normalized();
216 const QVector3D pos = center + normal * r;
217
218 // position
219 vertices.append(pos.x());
220 vertices.append(pos.y());
221 vertices.append(pos.z());
222 // normal
223 vertices.append(normal.x());
224 vertices.append(normal.y());
225 vertices.append(normal.z());
226 }
227 }
228
229 // Side triangles (connect ring 0 to ring 1)
230 for (int i = 0; i < cylinderSides; ++i) {
231 const unsigned int i0 = baseIdx + static_cast<unsigned int>(i);
232 const unsigned int i1 = baseIdx + static_cast<unsigned int>((i + 1) % cylinderSides);
233 const unsigned int i2 = i0 + static_cast<unsigned int>(cylinderSides);
234 const unsigned int i3 = i1 + static_cast<unsigned int>(cylinderSides);
235
236 // Two triangles per quad
237 indices.append(i0);
238 indices.append(i2);
239 indices.append(i1);
240 indices.append(i1);
241 indices.append(i2);
242 indices.append(i3);
243 }
244
245 // Tip endcap (ring 0, center = tipPos)
246 {
247 const QVector3D normal = -axisNorm;
248 const unsigned int centerIdx = static_cast<unsigned int>(vertices.size() / 6);
249 vertices.append(tipPos.x());
250 vertices.append(tipPos.y());
251 vertices.append(tipPos.z());
252 vertices.append(normal.x());
253 vertices.append(normal.y());
254 vertices.append(normal.z());
255
256 for (int i = 0; i < cylinderSides; ++i) {
257 const unsigned int i0 = baseIdx + static_cast<unsigned int>(i);
258 const unsigned int i1 = baseIdx + static_cast<unsigned int>((i + 1) % cylinderSides);
259 indices.append(centerIdx);
260 indices.append(i1);
261 indices.append(i0);
262 }
263 }
264
265 // Tail endcap (ring 1, center = tailPos)
266 {
267 const QVector3D normal = axisNorm;
268 const unsigned int centerIdx = static_cast<unsigned int>(vertices.size() / 6);
269 vertices.append(tailPos.x());
270 vertices.append(tailPos.y());
271 vertices.append(tailPos.z());
272 vertices.append(normal.x());
273 vertices.append(normal.y());
274 vertices.append(normal.z());
275
276 const unsigned int ring1Base = baseIdx + static_cast<unsigned int>(cylinderSides);
277 for (int i = 0; i < cylinderSides; ++i) {
278 const unsigned int i0 = ring1Base + static_cast<unsigned int>(i);
279 const unsigned int i1 = ring1Base + static_cast<unsigned int>((i + 1) % cylinderSides);
280 indices.append(centerIdx);
281 indices.append(i0);
282 indices.append(i1);
283 }
284 }
285 }
286}
287
288//=============================================================================================================
289
290void ElectrodeObject::generateContactInstances(QVector<float>& instanceData) const
291{
292 instanceData.clear();
293 const int floatsPerInstance = 9;
294 instanceData.reserve(totalContactCount() * floatsPerInstance);
295
296 for (const auto& shaft : m_arrays) {
297 for (const auto& contact : shaft.contacts) {
298 // position (3)
299 instanceData.append(contact.position.x());
300 instanceData.append(contact.position.y());
301 instanceData.append(contact.position.z());
302 // radius (1)
303 instanceData.append(contact.radius);
304 // color RGBA (4)
305 instanceData.append(static_cast<float>(contact.color.redF()));
306 instanceData.append(static_cast<float>(contact.color.greenF()));
307 instanceData.append(static_cast<float>(contact.color.blueF()));
308 instanceData.append(static_cast<float>(contact.color.alphaF()));
309 // selected flag (1)
310 instanceData.append(contact.selected ? 1.0f : 0.0f);
311 }
312 }
313}
314
315//=============================================================================================================
316
318{
319 return m_bbMin;
320}
321
322//=============================================================================================================
323
325{
326 return m_bbMax;
327}
328
329//=============================================================================================================
330
331void ElectrodeObject::computeBoundingBox()
332{
333 m_bbMin = QVector3D(std::numeric_limits<float>::max(),
334 std::numeric_limits<float>::max(),
335 std::numeric_limits<float>::max());
336 m_bbMax = QVector3D(std::numeric_limits<float>::lowest(),
337 std::numeric_limits<float>::lowest(),
338 std::numeric_limits<float>::lowest());
339
340 for (const auto& shaft : m_arrays) {
341 for (const auto& contact : shaft.contacts) {
342 const float pad = contact.radius;
343 const QVector3D& p = contact.position;
344
345 if (p.x() - pad < m_bbMin.x())
346 m_bbMin.setX(p.x() - pad);
347 if (p.y() - pad < m_bbMin.y())
348 m_bbMin.setY(p.y() - pad);
349 if (p.z() - pad < m_bbMin.z())
350 m_bbMin.setZ(p.z() - pad);
351
352 if (p.x() + pad > m_bbMax.x())
353 m_bbMax.setX(p.x() + pad);
354 if (p.y() + pad > m_bbMax.y())
355 m_bbMax.setY(p.y() + pad);
356 if (p.z() + pad > m_bbMax.z())
357 m_bbMax.setZ(p.z() + pad);
358 }
359 }
360}
361
362//=============================================================================================================
363
364QColor ElectrodeObject::interpolateColor(float value, float minVal, float maxVal,
365 const QColor& minColor, const QColor& maxColor)
366{
367 if (maxVal <= minVal)
368 return minColor;
369
370 float t = (value - minVal) / (maxVal - minVal);
371 t = qBound(0.0f, t, 1.0f);
372
373 const float r = static_cast<float>(minColor.redF()) * (1.0f - t) + static_cast<float>(maxColor.redF()) * t;
374 const float g = static_cast<float>(minColor.greenF()) * (1.0f - t) + static_cast<float>(maxColor.greenF()) * t;
375 const float b = static_cast<float>(minColor.blueF()) * (1.0f - t) + static_cast<float>(maxColor.blueF()) * t;
376 const float a = static_cast<float>(minColor.alphaF()) * (1.0f - t) + static_cast<float>(maxColor.alphaF()) * t;
377
378 QColor result;
379 result.setRedF(static_cast<qreal>(r));
380 result.setGreenF(static_cast<qreal>(g));
381 result.setBlueF(static_cast<qreal>(b));
382 result.setAlphaF(static_cast<qreal>(a));
383 return result;
384}
385
386//=============================================================================================================
387
388void ElectrodeObject::updateBuffers(QRhi* rhi, QRhiResourceUpdateBatch* u)
389{
390 const bool needsCreate = !m_gpu->vertexBuffer || !m_gpu->indexBuffer || !m_gpu->instanceBuffer;
391
392#ifdef __EMSCRIPTEN__
393 if (!needsCreate && !m_gpu->dirty) {
394 // WASM: always re-upload to avoid VAO cache staleness
395 if (m_gpu->shaftIndexCount > 0) {
396 QVector<float> verts;
397 QVector<unsigned int> idx;
398 generateShaftGeometry(verts, idx);
399 u->uploadStaticBuffer(m_gpu->vertexBuffer.get(), verts.constData());
400 u->uploadStaticBuffer(m_gpu->indexBuffer.get(), idx.constData());
401 }
402 if (m_gpu->instanceCount > 0) {
403 QVector<float> inst;
405 u->uploadStaticBuffer(m_gpu->instanceBuffer.get(), inst.constData());
406 }
407 return;
408 }
409#else
410 if (!m_gpu->dirty && !needsCreate)
411 return;
412#endif
413
414 // Generate CPU-side data
415 QVector<float> shaftVerts;
416 QVector<unsigned int> shaftIdx;
417 generateShaftGeometry(shaftVerts, shaftIdx);
418
419 QVector<float> instData;
420 generateContactInstances(instData);
421
422 m_gpu->shaftIndexCount = static_cast<uint32_t>(shaftIdx.size());
423 m_gpu->instanceCount = static_cast<uint32_t>(instData.size() / 9);
424
425 // Shaft vertex buffer
426 const quint32 vbufSize = static_cast<quint32>(shaftVerts.size() * sizeof(float));
427 if (vbufSize > 0) {
428 if (!m_gpu->vertexBuffer) {
429 m_gpu->vertexBuffer.reset(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, vbufSize));
430 m_gpu->vertexBuffer->create();
431 }
432 u->uploadStaticBuffer(m_gpu->vertexBuffer.get(), shaftVerts.constData());
433 }
434
435 // Shaft index buffer
436 const quint32 ibufSize = static_cast<quint32>(shaftIdx.size() * sizeof(unsigned int));
437 if (ibufSize > 0) {
438 if (!m_gpu->indexBuffer) {
439 m_gpu->indexBuffer.reset(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, ibufSize));
440 m_gpu->indexBuffer->create();
441 }
442 u->uploadStaticBuffer(m_gpu->indexBuffer.get(), shaftIdx.constData());
443 }
444
445 // Contact instance buffer
446 const quint32 instBufSize = static_cast<quint32>(instData.size() * sizeof(float));
447 if (instBufSize > 0) {
448 if (!m_gpu->instanceBuffer) {
449 m_gpu->instanceBuffer.reset(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, instBufSize));
450 m_gpu->instanceBuffer->create();
451 }
452 u->uploadStaticBuffer(m_gpu->instanceBuffer.get(), instData.constData());
453 }
454
455 m_gpu->dirty = false;
456}
457
458//=============================================================================================================
459
461{
462 return m_gpu->vertexBuffer.get();
463}
464
465//=============================================================================================================
466
468{
469 return m_gpu->indexBuffer.get();
470}
471
472//=============================================================================================================
473
475{
476 return m_gpu->instanceBuffer.get();
477}
478
479//=============================================================================================================
480
482{
483 return m_gpu->shaftIndexCount;
484}
485
486//=============================================================================================================
487
489{
490 return m_gpu->instanceCount;
491}
#define M_PI
ECoG / sEEG electrode model: shaft cylinders, contact spheres and grid layouts mapped to a colour-bar...
3-D brain visualisation using the Qt RHI rendering backend.
std::unique_ptr< QRhiBuffer > indexBuffer
std::unique_ptr< QRhiBuffer > vertexBuffer
std::unique_ptr< QRhiBuffer > instanceBuffer
void generateShaftGeometry(QVector< float > &vertices, QVector< unsigned int > &indices, int cylinderSides=16) const
QRhiBuffer * instanceBuffer() const
void generateContactInstances(QVector< float > &instanceData) const
void setArrays(const QVector< ElectrodeArray > &arrays)
QRhiBuffer * indexBuffer() const
void updateBuffers(QRhi *rhi, QRhiResourceUpdateBatch *u)
QRhiBuffer * vertexBuffer() const
void setContactValues(const QMap< QString, float > &values, const QColor &minColor=Qt::blue, const QColor &maxColor=Qt::red)
const QVector< ElectrodeArray > & arrays() const
void selectContact(const QString &name)
uint32_t contactInstanceCount() const