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) minVal = it.value();
105 if (it.value() > maxVal) maxVal = it.value();
106 }
107
108 // Apply to contacts
109 for (auto& shaft : m_arrays) {
110 for (auto& contact : shaft.contacts) {
111 auto it = values.find(contact.name);
112 if (it != values.end()) {
113 contact.value = it.value();
114 contact.color = interpolateColor(it.value(), minVal, maxVal,
115 minColor, maxColor);
116 }
117 }
118 }
119 m_gpu->dirty = true;
120}
121
122//=============================================================================================================
123
124void ElectrodeObject::selectContact(const QString& name)
125{
126 // Clear previous selection
128
129 // Set new selection
130 m_selectedContact = name;
131 for (auto& shaft : m_arrays) {
132 for (auto& contact : shaft.contacts) {
133 if (contact.name == name) {
134 contact.selected = true;
135 m_gpu->dirty = true;
136 return;
137 }
138 }
139 }
140
141 // Not found — clear name
142 m_selectedContact.clear();
143}
144
145//=============================================================================================================
146
148{
149 m_selectedContact.clear();
150 for (auto& shaft : m_arrays) {
151 for (auto& contact : shaft.contacts)
152 contact.selected = false;
153 }
154 m_gpu->dirty = true;
155}
156
157//=============================================================================================================
158
160{
161 return m_selectedContact;
162}
163
164//=============================================================================================================
165
166void ElectrodeObject::generateShaftGeometry(QVector<float>& vertices,
167 QVector<unsigned int>& indices,
168 int cylinderSides) const
169{
170 vertices.clear();
171 indices.clear();
172
173 if (cylinderSides < 3)
174 cylinderSides = 3;
175
176 for (const auto& shaft : m_arrays) {
177 // v2.3.0: Strip and Grid layouts render as instanced spheres only
178 // (no cylinder shaft). They are emitted via generateContactInstances.
179 if (shaft.layout != ElectrodeLayout::Depth)
180 continue;
181 if (shaft.contacts.size() < 2)
182 continue;
183
184 const QVector3D& tipPos = shaft.contacts.first().position;
185 const QVector3D& tailPos = shaft.contacts.last().position;
186 const QVector3D axis = tailPos - tipPos;
187 const float length = axis.length();
188 if (length < 1e-6f)
189 continue;
190
191 const QVector3D axisNorm = axis.normalized();
192
193 // Build a perpendicular basis
194 QVector3D perp;
195 if (qAbs(QVector3D::dotProduct(axisNorm, QVector3D(0, 1, 0))) < 0.99f)
196 perp = QVector3D::crossProduct(axisNorm, QVector3D(0, 1, 0)).normalized();
197 else
198 perp = QVector3D::crossProduct(axisNorm, QVector3D(1, 0, 0)).normalized();
199
200 const QVector3D biperp = QVector3D::crossProduct(axisNorm, perp).normalized();
201
202 const float r = shaft.shaftRadius;
203 const unsigned int baseIdx = static_cast<unsigned int>(vertices.size() / 6);
204
205 // Generate circle vertices at tip and tail
206 for (int ring = 0; ring < 2; ++ring) {
207 const QVector3D center = (ring == 0) ? tipPos : tailPos;
208 for (int i = 0; i < cylinderSides; ++i) {
209 const float angle = 2.0f * float(M_PI) * float(i) / float(cylinderSides);
210 const float cs = cosf(angle);
211 const float sn = sinf(angle);
212
213 const QVector3D normal = (perp * cs + biperp * sn).normalized();
214 const QVector3D pos = center + normal * r;
215
216 // position
217 vertices.append(pos.x());
218 vertices.append(pos.y());
219 vertices.append(pos.z());
220 // normal
221 vertices.append(normal.x());
222 vertices.append(normal.y());
223 vertices.append(normal.z());
224 }
225 }
226
227 // Side triangles (connect ring 0 to ring 1)
228 for (int i = 0; i < cylinderSides; ++i) {
229 const unsigned int i0 = baseIdx + static_cast<unsigned int>(i);
230 const unsigned int i1 = baseIdx + static_cast<unsigned int>((i + 1) % cylinderSides);
231 const unsigned int i2 = i0 + static_cast<unsigned int>(cylinderSides);
232 const unsigned int i3 = i1 + static_cast<unsigned int>(cylinderSides);
233
234 // Two triangles per quad
235 indices.append(i0); indices.append(i2); indices.append(i1);
236 indices.append(i1); indices.append(i2); indices.append(i3);
237 }
238
239 // Tip endcap (ring 0, center = tipPos)
240 {
241 const QVector3D normal = -axisNorm;
242 const unsigned int centerIdx = static_cast<unsigned int>(vertices.size() / 6);
243 vertices.append(tipPos.x());
244 vertices.append(tipPos.y());
245 vertices.append(tipPos.z());
246 vertices.append(normal.x());
247 vertices.append(normal.y());
248 vertices.append(normal.z());
249
250 for (int i = 0; i < cylinderSides; ++i) {
251 const unsigned int i0 = baseIdx + static_cast<unsigned int>(i);
252 const unsigned int i1 = baseIdx + static_cast<unsigned int>((i + 1) % cylinderSides);
253 indices.append(centerIdx); indices.append(i1); indices.append(i0);
254 }
255 }
256
257 // Tail endcap (ring 1, center = tailPos)
258 {
259 const QVector3D normal = axisNorm;
260 const unsigned int centerIdx = static_cast<unsigned int>(vertices.size() / 6);
261 vertices.append(tailPos.x());
262 vertices.append(tailPos.y());
263 vertices.append(tailPos.z());
264 vertices.append(normal.x());
265 vertices.append(normal.y());
266 vertices.append(normal.z());
267
268 const unsigned int ring1Base = baseIdx + static_cast<unsigned int>(cylinderSides);
269 for (int i = 0; i < cylinderSides; ++i) {
270 const unsigned int i0 = ring1Base + static_cast<unsigned int>(i);
271 const unsigned int i1 = ring1Base + static_cast<unsigned int>((i + 1) % cylinderSides);
272 indices.append(centerIdx); indices.append(i0); indices.append(i1);
273 }
274 }
275 }
276}
277
278//=============================================================================================================
279
280void ElectrodeObject::generateContactInstances(QVector<float>& instanceData) const
281{
282 instanceData.clear();
283 const int floatsPerInstance = 9;
284 instanceData.reserve(totalContactCount() * floatsPerInstance);
285
286 for (const auto& shaft : m_arrays) {
287 for (const auto& contact : shaft.contacts) {
288 // position (3)
289 instanceData.append(contact.position.x());
290 instanceData.append(contact.position.y());
291 instanceData.append(contact.position.z());
292 // radius (1)
293 instanceData.append(contact.radius);
294 // color RGBA (4)
295 instanceData.append(static_cast<float>(contact.color.redF()));
296 instanceData.append(static_cast<float>(contact.color.greenF()));
297 instanceData.append(static_cast<float>(contact.color.blueF()));
298 instanceData.append(static_cast<float>(contact.color.alphaF()));
299 // selected flag (1)
300 instanceData.append(contact.selected ? 1.0f : 0.0f);
301 }
302 }
303}
304
305//=============================================================================================================
306
308{
309 return m_bbMin;
310}
311
312//=============================================================================================================
313
315{
316 return m_bbMax;
317}
318
319//=============================================================================================================
320
321void ElectrodeObject::computeBoundingBox()
322{
323 m_bbMin = QVector3D(std::numeric_limits<float>::max(),
324 std::numeric_limits<float>::max(),
325 std::numeric_limits<float>::max());
326 m_bbMax = QVector3D(std::numeric_limits<float>::lowest(),
327 std::numeric_limits<float>::lowest(),
328 std::numeric_limits<float>::lowest());
329
330 for (const auto& shaft : m_arrays) {
331 for (const auto& contact : shaft.contacts) {
332 const float pad = contact.radius;
333 const QVector3D& p = contact.position;
334
335 if (p.x() - pad < m_bbMin.x()) m_bbMin.setX(p.x() - pad);
336 if (p.y() - pad < m_bbMin.y()) m_bbMin.setY(p.y() - pad);
337 if (p.z() - pad < m_bbMin.z()) m_bbMin.setZ(p.z() - pad);
338
339 if (p.x() + pad > m_bbMax.x()) m_bbMax.setX(p.x() + pad);
340 if (p.y() + pad > m_bbMax.y()) m_bbMax.setY(p.y() + pad);
341 if (p.z() + pad > m_bbMax.z()) m_bbMax.setZ(p.z() + pad);
342 }
343 }
344}
345
346//=============================================================================================================
347
348QColor ElectrodeObject::interpolateColor(float value, float minVal, float maxVal,
349 const QColor& minColor, const QColor& maxColor)
350{
351 if (maxVal <= minVal)
352 return minColor;
353
354 float t = (value - minVal) / (maxVal - minVal);
355 t = qBound(0.0f, t, 1.0f);
356
357 const float r = static_cast<float>(minColor.redF()) * (1.0f - t) + static_cast<float>(maxColor.redF()) * t;
358 const float g = static_cast<float>(minColor.greenF()) * (1.0f - t) + static_cast<float>(maxColor.greenF()) * t;
359 const float b = static_cast<float>(minColor.blueF()) * (1.0f - t) + static_cast<float>(maxColor.blueF()) * t;
360 const float a = static_cast<float>(minColor.alphaF()) * (1.0f - t) + static_cast<float>(maxColor.alphaF()) * t;
361
362 QColor result;
363 result.setRedF(static_cast<qreal>(r));
364 result.setGreenF(static_cast<qreal>(g));
365 result.setBlueF(static_cast<qreal>(b));
366 result.setAlphaF(static_cast<qreal>(a));
367 return result;
368}
369
370//=============================================================================================================
371
372void ElectrodeObject::updateBuffers(QRhi *rhi, QRhiResourceUpdateBatch *u)
373{
374 const bool needsCreate = !m_gpu->vertexBuffer || !m_gpu->indexBuffer || !m_gpu->instanceBuffer;
375
376#ifdef __EMSCRIPTEN__
377 if (!needsCreate && !m_gpu->dirty) {
378 // WASM: always re-upload to avoid VAO cache staleness
379 if (m_gpu->shaftIndexCount > 0) {
380 QVector<float> verts;
381 QVector<unsigned int> idx;
382 generateShaftGeometry(verts, idx);
383 u->uploadStaticBuffer(m_gpu->vertexBuffer.get(), verts.constData());
384 u->uploadStaticBuffer(m_gpu->indexBuffer.get(), idx.constData());
385 }
386 if (m_gpu->instanceCount > 0) {
387 QVector<float> inst;
389 u->uploadStaticBuffer(m_gpu->instanceBuffer.get(), inst.constData());
390 }
391 return;
392 }
393#else
394 if (!m_gpu->dirty && !needsCreate) return;
395#endif
396
397 // Generate CPU-side data
398 QVector<float> shaftVerts;
399 QVector<unsigned int> shaftIdx;
400 generateShaftGeometry(shaftVerts, shaftIdx);
401
402 QVector<float> instData;
403 generateContactInstances(instData);
404
405 m_gpu->shaftIndexCount = static_cast<uint32_t>(shaftIdx.size());
406 m_gpu->instanceCount = static_cast<uint32_t>(instData.size() / 9);
407
408 // Shaft vertex buffer
409 const quint32 vbufSize = static_cast<quint32>(shaftVerts.size() * sizeof(float));
410 if (vbufSize > 0) {
411 if (!m_gpu->vertexBuffer) {
412 m_gpu->vertexBuffer.reset(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, vbufSize));
413 m_gpu->vertexBuffer->create();
414 }
415 u->uploadStaticBuffer(m_gpu->vertexBuffer.get(), shaftVerts.constData());
416 }
417
418 // Shaft index buffer
419 const quint32 ibufSize = static_cast<quint32>(shaftIdx.size() * sizeof(unsigned int));
420 if (ibufSize > 0) {
421 if (!m_gpu->indexBuffer) {
422 m_gpu->indexBuffer.reset(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, ibufSize));
423 m_gpu->indexBuffer->create();
424 }
425 u->uploadStaticBuffer(m_gpu->indexBuffer.get(), shaftIdx.constData());
426 }
427
428 // Contact instance buffer
429 const quint32 instBufSize = static_cast<quint32>(instData.size() * sizeof(float));
430 if (instBufSize > 0) {
431 if (!m_gpu->instanceBuffer) {
432 m_gpu->instanceBuffer.reset(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, instBufSize));
433 m_gpu->instanceBuffer->create();
434 }
435 u->uploadStaticBuffer(m_gpu->instanceBuffer.get(), instData.constData());
436 }
437
438 m_gpu->dirty = false;
439}
440
441//=============================================================================================================
442
444{
445 return m_gpu->vertexBuffer.get();
446}
447
448//=============================================================================================================
449
451{
452 return m_gpu->indexBuffer.get();
453}
454
455//=============================================================================================================
456
458{
459 return m_gpu->instanceBuffer.get();
460}
461
462//=============================================================================================================
463
465{
466 return m_gpu->shaftIndexCount;
467}
468
469//=============================================================================================================
470
472{
473 return m_gpu->instanceCount;
474}
ECoG / sEEG electrode model: shaft cylinders, contact spheres and grid layouts mapped to a colour-bar...
#define M_PI
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