v2.0.0
Loading...
Searching...
No Matches
brainsurface.cpp
Go to the documentation of this file.
1//=============================================================================================================
12
13//=============================================================================================================
14// INCLUDES
15//=============================================================================================================
16
17#include "brainsurface.h"
18
19#include <rhi/qrhi.h>
20
21#include <set>
22
23namespace DISP3DLIB
24{
25
26namespace
27{
28uint32_t withAlpha(uint32_t color, uint32_t alpha)
29{
30 return (color & 0x00FFFFFFu) | ((alpha & 0xFFu) << 24);
31}
32
33uint32_t curvatureGray(const QVector<float>& curvature, int index)
34{
35 return (index >= 0 && index < curvature.size() && curvature[index] > 0.0f)
36 ? 0x40u
37 : 0xAAu;
38}
39
40// Overlay alpha is blended into RGB here because the shader keeps the curvature grey in the alpha channel.
41uint32_t overCortex(uint32_t overlay, uint32_t gray)
42{
43 const uint32_t a = overlay >> 24;
44 uint32_t rgb = 0;
45 for (int shift = 0; shift < 24; shift += 8) {
46 const uint32_t c = (overlay >> shift) & 0xFFu;
47 rgb |= ((c * a + gray * (255u - a) + 127u) / 255u) << shift;
48 }
49 return withAlpha(rgb, gray);
50}
51} // namespace
52
53//=============================================================================================================
54// PIMPL
55//=============================================================================================================
56
58{
59 std::unique_ptr<QRhiBuffer> vertexBuffer;
60 std::unique_ptr<QRhiBuffer> indexBuffer;
61 bool dirty = true;
62 bool indexDirty = true; // IBO needs (re-)upload (topology change)
63};
64
65//=============================================================================================================
66
67void BrainSurface::markVertexDirty()
68{
69 m_gpu->dirty = true;
70 ++m_vertexGeneration;
71}
72
73
74//=============================================================================================================
75// DEFINE MEMBER METHODS
76//=============================================================================================================
77
78
79//=============================================================================================================
80
82: m_gpu(std::make_unique<GpuBuffers>())
83{
84}
85
86//=============================================================================================================
87
89
90//=============================================================================================================
91
92QRhiBuffer* BrainSurface::vertexBuffer() const
93{
94 return m_gpu->vertexBuffer.get();
95}
96QRhiBuffer* BrainSurface::indexBuffer() const
97{
98 return m_gpu->indexBuffer.get();
99}
100
101//=============================================================================================================
102
104{
105 m_vertexData.clear();
106 m_indexData.clear();
107
108 const Eigen::MatrixXf& rr = surf.rr();
109 const Eigen::MatrixXf& nn = surf.nn();
110 const Eigen::MatrixXi& tris = surf.tris();
111 const Eigen::VectorXf& curv = surf.curv();
112 m_curvature.resize(curv.size());
113 for (int i = 0; i < curv.size(); ++i)
114 m_curvature[i] = curv[i];
115
116 // Populate vertex data
117 m_vertexData.reserve(rr.rows());
118
119 for (int i = 0; i < rr.rows(); ++i) {
120 VertexData v;
121 v.pos = QVector3D(rr(i, 0), rr(i, 1), rr(i, 2));
122 v.norm = QVector3D(nn(i, 0), nn(i, 1), nn(i, 2));
123 v.color = 0xFFFFFFFF; // Default white (overwritten by updateVertexColors)
124 v.colorAnnotation = 0x00000000; // No annotation yet
125 m_vertexData.append(v);
126 }
127
128 m_indexData.reserve(tris.rows() * 3);
129 for (int i = 0; i < tris.rows(); ++i) {
130 m_indexData.append(tris(i, 0));
131 m_indexData.append(tris(i, 1));
132 m_indexData.append(tris(i, 2));
133 }
134 m_indexCount = m_indexData.size();
135
136 markVertexDirty();
137 m_bAABBDirty = true;
138
139 // Initial coloring based on current visualization mode
140 updateVertexColors();
141
142 m_originalVertexData = m_vertexData;
143}
144
145//=============================================================================================================
146
147//=============================================================================================================
148
149void BrainSurface::fromBemSurface(const MNELIB::MNEBemSurface& surf, const QColor& color)
150{
151 m_vertexData.clear();
152 m_indexData.clear();
153 m_curvature.clear(); // BEM has no curvature info usually
154
155 int nVerts = surf.rr.rows();
156 m_vertexData.reserve(nVerts);
157
158 // Compute normals if missing
159 Eigen::MatrixX3f nn = surf.nn;
160 if (nn.rows() != nVerts) {
161 nn = FSLIB::FsSurface::compute_normals(Eigen::MatrixX3f(surf.rr), Eigen::MatrixX3i(surf.itris));
162 }
163
164 m_defaultColor = color;
165 m_baseColor = color;
166 uint32_t colorVal = packABGR(color.red(), color.green(), color.blue(), color.alpha());
167
168 for (int i = 0; i < nVerts; ++i) {
169 VertexData v;
170 v.pos = QVector3D(surf.rr(i, 0), surf.rr(i, 1), surf.rr(i, 2));
171 v.norm = QVector3D(nn(i, 0), nn(i, 1), nn(i, 2));
172 v.color = colorVal;
173 v.colorAnnotation = 0x00000000;
174 m_vertexData.append(v);
175 }
176
177 int nTris = surf.itris.rows();
178 m_indexData.reserve(nTris * 3);
179 for (int i = 0; i < nTris; ++i) {
180 m_indexData.append(surf.itris(i, 0));
181 m_indexData.append(surf.itris(i, 1));
182 m_indexData.append(surf.itris(i, 2));
183 }
184 m_indexCount = m_indexData.size();
185
186 m_originalVertexData = m_vertexData;
187 markVertexDirty();
188}
189
190void BrainSurface::createFromData(const Eigen::MatrixX3f& vertices, const Eigen::MatrixX3i& triangles, const QColor& color)
191{
192 createFromData(vertices, FSLIB::FsSurface::compute_normals(vertices, triangles), triangles, color);
193}
194
195void BrainSurface::createFromData(const Eigen::MatrixX3f& vertices, const Eigen::MatrixX3f& normals, const Eigen::MatrixX3i& triangles, const QColor& color)
196{
197 m_vertexData.clear();
198 m_indexData.clear();
199 m_curvature.clear();
200
201 int nVerts = vertices.rows();
202 m_vertexData.reserve(nVerts);
203
204 m_defaultColor = color;
205 m_baseColor = color;
206 uint32_t colorVal = packABGR(color.red(), color.green(), color.blue(), color.alpha());
207
208 for (int i = 0; i < nVerts; ++i) {
209 VertexData v;
210 v.pos = QVector3D(vertices(i, 0), vertices(i, 1), vertices(i, 2));
211 v.norm = QVector3D(normals(i, 0), normals(i, 1), normals(i, 2));
212 v.color = colorVal;
213 v.colorAnnotation = 0x00000000;
214 m_vertexData.append(v);
215 }
216
217 int nTris = triangles.rows();
218 m_indexData.reserve(nTris * 3);
219 for (int i = 0; i < nTris; ++i) {
220 m_indexData.append(triangles(i, 0));
221 m_indexData.append(triangles(i, 1));
222 m_indexData.append(triangles(i, 2));
223 }
224 m_indexCount = m_indexData.size();
225
226 m_originalVertexData = m_vertexData;
227 markVertexDirty();
228}
229
230//=============================================================================================================
231
232Eigen::MatrixX3f BrainSurface::vertexPositions() const
233{
234 Eigen::MatrixX3f rr(m_vertexData.size(), 3);
235 for (int i = 0; i < m_vertexData.size(); ++i) {
236 rr(i, 0) = m_vertexData[i].pos.x();
237 rr(i, 1) = m_vertexData[i].pos.y();
238 rr(i, 2) = m_vertexData[i].pos.z();
239 }
240 return rr;
241}
242
243//=============================================================================================================
244
245Eigen::MatrixX3f BrainSurface::vertexNormals() const
246{
247 Eigen::MatrixX3f nn(m_vertexData.size(), 3);
248 for (int i = 0; i < m_vertexData.size(); ++i) {
249 nn(i, 0) = m_vertexData[i].norm.x();
250 nn(i, 1) = m_vertexData[i].norm.y();
251 nn(i, 2) = m_vertexData[i].norm.z();
252 }
253 return nn;
254}
255
256//=============================================================================================================
257
258bool BrainSurface::loadAnnotation(const QString& path)
259{
260 if (!FSLIB::FsAnnotation::read(path, m_annotation)) {
261 qWarning() << "BrainSurface: Failed to load annotation from" << path;
262 return false;
263 }
264 m_hasAnnotation = true;
265 updateVertexColors();
266 markVertexDirty();
267 return true;
268}
269
271{
272 m_annotation = annotation;
273 m_hasAnnotation = true;
274 updateVertexColors();
275 markVertexDirty();
276}
277
278
279//=============================================================================================================
280
281void BrainSurface::setVisible(bool visible)
282{
283 m_visible = visible;
284}
285
286//=============================================================================================================
287
289{
290 if (m_visMode == mode)
291 return;
292
293 m_visMode = mode;
294
295 // Scientific and SourceEstimate both read from the primary colour
296 // channel (v_color). When switching between them the vertex buffer
297 // must be refreshed so the channel holds curvature grays (Scientific)
298 // or STC colours (SourceEstimate).
299 updateVertexColors();
300 markVertexDirty();
301}
302
303//=============================================================================================================
304
305void BrainSurface::applySourceEstimateColors(const QVector<uint32_t>& colors)
306{
307 m_visMode = ModeSourceEstimate;
308 m_stcColors = colors;
309
310 // Write STC colours into the primary color channel. Preserve neutral
311 // curvature grey in alpha so Surface mode can still render the classic
312 // light/dark cortex while RGB is occupied by source-estimate colours.
313 for (int i = 0; i < qMin(colors.size(), m_vertexData.size()); ++i) {
314 m_vertexData[i].color = overCortex(colors[i], curvatureGray(m_curvature, i));
315 }
316
317 markVertexDirty();
318}
319
320//=============================================================================================================
321
323{
324 m_stcColors.clear();
325 m_visMode = ModeSurface;
326 updateVertexColors();
327 markVertexDirty();
328}
329
330//=============================================================================================================
331
333{
334 m_baseColor = useDefault ? m_defaultColor : Qt::white;
335 updateVertexColors();
336}
337
338//=============================================================================================================
339
340void BrainSurface::setColor(const QColor& color)
341{
342 m_defaultColor = color;
343 m_baseColor = color;
344 updateVertexColors();
345}
346
347void BrainSurface::updateVertexColors()
348{
349 // ── 1. Populate the primary "color" channel.
350 // Brain surfaces (with curvature): curvature-based gray.
351 // Non-brain surfaces (BEM, sensors, etc.): use m_baseColor.
352 // The shader uses this for Scientific mode and as a lighting base;
353 // in FsSurface mode the shader overrides with white for brain tissue.
354 if (!m_curvature.isEmpty() && m_curvature.size() == m_vertexData.size()) {
355 for (int i = 0; i < m_vertexData.size(); ++i) {
356 const uint32_t val = curvatureGray(m_curvature, i);
357 m_vertexData[i].color = packABGR(val, val, val, val);
358 }
359 } else {
360 // Use the base colour (set via setUseDefaultColor / fromBemSurface).
361 // This preserves BEM Red/Green/Blue colours and sensor colours.
362 const uint32_t baseVal = packABGR(m_baseColor.red(), m_baseColor.green(),
363 m_baseColor.blue(), m_baseColor.alpha());
364 for (int i = 0; i < m_vertexData.size(); ++i) {
365 m_vertexData[i].color = baseVal;
366 }
367 }
368
369 // Always keep STC colours in the primary colour channel when available.
370 // Each viewport selects the overlay mode via a per-draw shader uniform
371 // (overlayMode), so the vertex buffer must hold STC data for any
372 // viewport that shows Source Estimate, regardless of this surface's
373 // m_visMode. FsAnnotation data lives in a separate vertex attribute
374 // (colorAnnotation) and is unaffected.
375 if (!m_stcColors.isEmpty()) {
376 for (int i = 0; i < qMin(m_stcColors.size(), m_vertexData.size()); ++i) {
377 m_vertexData[i].color = overCortex(m_stcColors[i], curvatureGray(m_curvature, i));
378 }
379 }
380
381 // ── 2. Populate colorAnnotation from loaded annotation data.
382 for (auto& v : m_vertexData) {
383 v.colorAnnotation = 0x00000000;
384 }
385
386 if (m_hasAnnotation && !m_vertexData.isEmpty()) {
387 const Eigen::VectorXi& vertices = m_annotation.getVertices();
388 const Eigen::VectorXi& labelIds = m_annotation.getLabelIds();
389 const FSLIB::FsColortable& ct = m_annotation.getColortable();
390
391 for (int i = 0; i < labelIds.rows(); ++i) {
392 int vertexIdx = vertices(i);
393 if (vertexIdx >= 0 && vertexIdx < m_vertexData.size()) {
394 int colorIdx = -1;
395 for (int c = 0; c < ct.numEntries; ++c) {
396 if (ct.table(c, 4) == labelIds(i)) {
397 colorIdx = c;
398 break;
399 }
400 }
401 if (colorIdx >= 0) {
402 uint32_t r = ct.table(colorIdx, 0);
403 uint32_t g = ct.table(colorIdx, 1);
404 uint32_t b = ct.table(colorIdx, 2);
405 m_vertexData[vertexIdx].colorAnnotation =
406 packABGR(r, g, b);
407 }
408 }
409 }
410 }
411
412 // ── 3. Selection highlighting.
413 // Tint the selected region / vertex range gold in the vertex
414 // buffer so the user gets precise per-region feedback.
415 // On WASM the merged-draw path re-reads vertexDataRef() every
416 // frame, so this is safe without separate buffer re-uploads.
417 if (m_selected) {
418 const uint32_t gold = packABGR(255, 200, 60);
419 if (m_selectedRegionId != -1 && m_hasAnnotation) {
420 // Highlight a specific annotation region
421 const Eigen::VectorXi& vertices = m_annotation.getVertices();
422 const Eigen::VectorXi& labelIds = m_annotation.getLabelIds();
423 for (int i = 0; i < labelIds.rows(); ++i) {
424 if (labelIds(i) == m_selectedRegionId) {
425 int idx = vertices(i);
426 if (idx >= 0 && idx < m_vertexData.size()) {
427 m_vertexData[idx].color = gold;
428 m_vertexData[idx].colorAnnotation = gold;
429 }
430 }
431 }
432 } else if (m_selectedVertexStart >= 0 && m_selectedVertexCount > 0) {
433 // Highlight a vertex range (e.g. single digitizer sphere)
434 const int end = qMin(m_selectedVertexStart + m_selectedVertexCount,
435 m_vertexData.size());
436 for (int i = m_selectedVertexStart; i < end; ++i) {
437 m_vertexData[i].color = gold;
438 }
439 }
440 // Whole-surface selection (no region / vertex range) is handled
441 // by the shader gold glow via the isSelected uniform.
442 }
443
444 markVertexDirty();
445}
446
448{
449 float minVal = std::numeric_limits<float>::max();
450 for (const auto& v : m_vertexData) {
451 if (v.pos.x() < minVal)
452 minVal = v.pos.x();
453 }
454 return minVal;
455}
456
458{
459 float maxVal = std::numeric_limits<float>::lowest();
460 for (const auto& v : m_vertexData) {
461 if (v.pos.x() > maxVal)
462 maxVal = v.pos.x();
463 }
464 return maxVal;
465}
466
467void BrainSurface::translateX(float offset)
468{
469 for (auto& v : m_vertexData) {
470 v.pos.setX(v.pos.x() + offset);
471 }
472 markVertexDirty();
473 m_bAABBDirty = true;
474}
475
476//=============================================================================================================
477
478void BrainSurface::transform(const QMatrix4x4& m)
479{
480 // Extract 3x3 normal matrix (inverse transpose of upper-left 3x3)
481 // QMatrix4x4::normalMatrix() returns QMatrix3x3.
482 QMatrix3x3 normalMat = m.normalMatrix();
483
484 for (auto& v : m_vertexData) {
485 // Transform position
486 v.pos = m.map(v.pos);
487
488 // Transform normal
489 // Note: QMatrix3x3 * QVector3D isn't directly supported by some Qt versions conveniently,
490 // but let's assume standard multiplication works or do manually.
491 // Actually QVector3D operator*(QMatrix4x4) exists but is row-vector mul.
492 // QMatrix4x4 operator*(QVector3D) is standard column-vector mul.
493
494 // Use generic map method or just manual multiply if needed.
495 // Easier: mapVector for vectors (ignores translation) but needs to be normal matrix for non-uniform scales.
496 // If scale is uniform, mapVector is fine.
497
498 // Let's do it manually to be safe with QMatrix3x3
499 const float* d = normalMat.constData();
500 float nx = d[0] * v.norm.x() + d[3] * v.norm.y() + d[6] * v.norm.z();
501 float ny = d[1] * v.norm.x() + d[4] * v.norm.y() + d[7] * v.norm.z();
502 float nz = d[2] * v.norm.x() + d[5] * v.norm.y() + d[8] * v.norm.z();
503 v.norm = QVector3D(nx, ny, nz).normalized();
504 }
505 markVertexDirty();
506 m_bAABBDirty = true;
507}
508
509//=============================================================================================================
510
511void BrainSurface::applyTransform(const QMatrix4x4& m)
512{
513 // Only the geometry is reset; colours and overlays set since loading stay
514 for (int i = 0; i < qMin(m_vertexData.size(), m_originalVertexData.size()); ++i) {
515 m_vertexData[i].pos = m_originalVertexData[i].pos;
516 m_vertexData[i].norm = m_originalVertexData[i].norm;
517 }
518 if (!m.isIdentity()) {
519 transform(m);
520 } else {
521 markVertexDirty();
522 m_bAABBDirty = true;
523 }
524}
525
526//=============================================================================================================
527
528void BrainSurface::updateBuffers(QRhi* rhi, QRhiResourceUpdateBatch* u)
529{
530 const bool needsCreate = !m_gpu->vertexBuffer || !m_gpu->indexBuffer;
531
532#ifdef __EMSCRIPTEN__
533 // On WebGL/WASM, always re-upload vertex and index data.
534 // The QRhi GLES2 backend's VAO cache can lose its element-buffer
535 // binding between frames, causing draws to produce no output.
536 // Re-uploading via uploadStaticBuffer triggers the necessary
537 // glBindBuffer calls that refresh the GL state.
538 if (!needsCreate && !m_gpu->dirty) {
539 // Buffers exist and data hasn't changed — still re-upload
540 u->uploadStaticBuffer(m_gpu->vertexBuffer.get(), m_vertexData.constData());
541 u->uploadStaticBuffer(m_gpu->indexBuffer.get(), m_indexData.constData());
542 return;
543 }
544#else
545 // Desktop: VBO is Immutable. Nothing to do unless the data has
546 // actually changed (markVertexDirty()) or the buffer hasn't been
547 // created yet. When dirty we recreate the VBO below to swap in the
548 // new vertex data — this is intentional: an Immutable buffer cannot
549 // be partially updated, and STC colour animation modifies the colour
550 // channel of every vertex anyway.
551 if (!m_gpu->dirty && !needsCreate)
552 return;
553#endif
554
555 const quint32 vbufSize = static_cast<quint32>(m_vertexData.size() * sizeof(VertexData));
556 const quint32 ibufSize = static_cast<quint32>(m_indexData.size() * sizeof(uint32_t));
557
558#ifdef __EMSCRIPTEN__
559 if (!m_gpu->vertexBuffer) {
560 m_gpu->vertexBuffer.reset(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, vbufSize));
561 m_gpu->vertexBuffer->create();
562 m_gpu->indexDirty = true; // new VBO implies IBO also needs upload
563 }
564#else
565 // Desktop: recreate the Immutable VBO whenever data is dirty so the
566 // new vertex contents take effect. Using Immutable (re-created on
567 // change) instead of QRhiBuffer::Dynamic avoids two issues with
568 // Dynamic buffers on Metal/Vulkan for multi-MB vertex data:
569 // 1) Dynamic is documented as intended for small, frequently-updated
570 // payloads (UBOs); large Dynamic buffers exhibit unstable
571 // behaviour across in-flight slot rotation, including stale-slot
572 // reads (frozen STC frames) and out-of-range geometry artefacts
573 // ("stretched triangle" glitches).
574 // 2) Each in-flight frame slot allocates its own physical buffer,
575 // multiplying memory pressure ~3x for every visible surface.
576 if (!m_gpu->vertexBuffer || m_gpu->dirty) {
577 m_gpu->vertexBuffer.reset(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, vbufSize));
578 m_gpu->vertexBuffer->create();
579 m_gpu->indexDirty = true; // new VBO implies IBO also needs upload
580 }
581#endif
582 if (!m_gpu->indexBuffer) {
583 m_gpu->indexBuffer.reset(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, ibufSize));
584 m_gpu->indexBuffer->create();
585 m_gpu->indexDirty = true;
586 }
587
588 u->uploadStaticBuffer(m_gpu->vertexBuffer.get(), m_vertexData.constData());
589 if (m_gpu->indexDirty) {
590 u->uploadStaticBuffer(m_gpu->indexBuffer.get(), m_indexData.constData());
591 m_gpu->indexDirty = false;
592 }
593 m_gpu->dirty = false;
594}
595
596//=============================================================================================================
597
598std::vector<Eigen::VectorXi> BrainSurface::computeNeighbors() const
599{
600 // Use temporary std::vector<std::set<int>> for dedup during construction
601 std::vector<std::set<int>> tempNeighbors(m_vertexData.size());
602
603 // Triangles are stored in m_indexData as triplets
604 for (int i = 0; i + 2 < m_indexData.size(); i += 3) {
605 int v0 = m_indexData[i];
606 int v1 = m_indexData[i + 1];
607 int v2 = m_indexData[i + 2];
608
609 // Add bidirectional edges (set handles dedup)
610 tempNeighbors[v0].insert(v1);
611 tempNeighbors[v0].insert(v2);
612 tempNeighbors[v1].insert(v0);
613 tempNeighbors[v1].insert(v2);
614 tempNeighbors[v2].insert(v0);
615 tempNeighbors[v2].insert(v1);
616 }
617
618 // Convert to std::vector<VectorXi>
619 std::vector<Eigen::VectorXi> neighbors(tempNeighbors.size());
620 for (size_t k = 0; k < tempNeighbors.size(); ++k) {
621 const auto& s = tempNeighbors[k];
622 neighbors[k].resize(static_cast<Eigen::Index>(s.size()));
623 Eigen::Index idx = 0;
624 for (int val : s) {
625 neighbors[k][idx++] = val;
626 }
627 }
628
629 return neighbors;
630}
631
632//=============================================================================================================
633
634Eigen::MatrixX3f BrainSurface::verticesAsMatrix() const
635{
636 Eigen::MatrixX3f mat(m_vertexData.size(), 3);
637 for (int i = 0; i < m_vertexData.size(); ++i) {
638 mat(i, 0) = m_vertexData[i].pos.x();
639 mat(i, 1) = m_vertexData[i].pos.y();
640 mat(i, 2) = m_vertexData[i].pos.z();
641 }
642 return mat;
643}
644
645
646//=============================================================================================================
647
648void BrainSurface::boundingBox(QVector3D& min, QVector3D& max) const
649{
650 if (!m_bAABBDirty) {
651 min = m_aabbMin;
652 max = m_aabbMax;
653 return;
654 }
655
656 if (m_vertexData.isEmpty()) {
657 min = QVector3D(0, 0, 0);
658 max = QVector3D(0, 0, 0);
659 return;
660 }
661
662 min = m_vertexData[0].pos;
663 max = m_vertexData[0].pos;
664
665 for (const auto& v : m_vertexData) {
666 min.setX(std::min(min.x(), v.pos.x()));
667 min.setY(std::min(min.y(), v.pos.y()));
668 min.setZ(std::min(min.z(), v.pos.z()));
669
670 max.setX(std::max(max.x(), v.pos.x()));
671 max.setY(std::max(max.y(), v.pos.y()));
672 max.setZ(std::max(max.z(), v.pos.z()));
673 }
674
675 m_aabbMin = min;
676 m_aabbMax = max;
677 m_bAABBDirty = false;
678}
679
680bool BrainSurface::intersects(const QVector3D& rayOrigin, const QVector3D& rayDir, float& dist, int& vertexIdx) const
681{
682 vertexIdx = -1;
683 if (m_vertexData.isEmpty())
684 return false;
685
686 // 1. AABB Check (Cached)
687 QVector3D min, max;
688 boundingBox(min, max);
689
690 // Ray-AABB slab method (Double Precision for stability)
691 double eps = 1e-4;
692 double origin[3] = {rayOrigin.x(), rayOrigin.y(), rayOrigin.z()};
693 double dir[3] = {rayDir.x(), rayDir.y(), rayDir.z()};
694 double minB[3] = {min.x() - eps, min.y() - eps, min.z() - eps};
695 double maxB[3] = {max.x() + eps, max.y() + eps, max.z() + eps};
696
697 // Extract individual components for triangle intersection (Möller–Trumbore)
698 double originX = origin[0], originY = origin[1], originZ = origin[2];
699 double dirX = dir[0], dirY = dir[1], dirZ = dir[2];
700
701 double tmin = -std::numeric_limits<double>::max();
702 double tmax = std::numeric_limits<double>::max();
703
704 for (int i = 0; i < 3; ++i) {
705 if (std::abs(dir[i]) < 1e-15) {
706 if (origin[i] < minB[i] || origin[i] > maxB[i])
707 return false;
708 } else {
709 double t1 = (minB[i] - origin[i]) / dir[i];
710 double t2 = (maxB[i] - origin[i]) / dir[i];
711 if (t1 > t2)
712 std::swap(t1, t2);
713 if (t1 > tmin)
714 tmin = t1;
715 if (t2 < tmax)
716 tmax = t2;
717 if (tmin > tmax)
718 return false;
719 }
720 }
721
722 if (tmax < 1e-7)
723 return false;
724
725 // 2. Triangle intersection
726 double closestDist = std::numeric_limits<double>::max();
727 bool hit = false;
728 int closestVert = -1;
729
730 // Brute-force triangle intersection using Double Precision Möller–Trumbore
731 // Note: For 100k+ vertices this is slow, ideally we'd use an Octree/BVH.
732 // However, since we only do this on mouse-over, results are usually acceptable if not too many surfaces are active.
733 for (int i = 0; i < m_indexData.size(); i += 3) {
734 int i0 = m_indexData[i];
735 int i1 = m_indexData[i + 1];
736 int i2 = m_indexData[i + 2];
737 const QVector3D& v0q = m_vertexData[i0].pos;
738 const QVector3D& v1q = m_vertexData[i1].pos;
739 const QVector3D& v2q = m_vertexData[i2].pos;
740
741 double v0x = v0q.x(), v0y = v0q.y(), v0z = v0q.z();
742 double v1x = v1q.x(), v1y = v1q.y(), v1z = v1q.z();
743 double v2x = v2q.x(), v2y = v2q.y(), v2z = v2q.z();
744
745 double edge1x = v1x - v0x, edge1y = v1y - v0y, edge1z = v1z - v0z;
746 double edge2x = v2x - v0x, edge2y = v2y - v0y, edge2z = v2z - v0z;
747
748 double hx = dirY * edge2z - dirZ * edge2y;
749 double hy = dirZ * edge2x - dirX * edge2z;
750 double hz = dirX * edge2y - dirY * edge2x;
751
752 double a = edge1x * hx + edge1y * hy + edge1z * hz;
753 if (std::abs(a) < 1e-18)
754 continue; // Purely parallel
755
756 double f = 1.0 / a;
757 double sx = originX - v0x, sy = originY - v0y, sz = originZ - v0z;
758 double u = f * (sx * hx + sy * hy + sz * hz);
759 if (u < -1e-7 || u > 1.0000001)
760 continue;
761
762 double qx = sy * edge1z - sz * edge1y;
763 double qy = sz * edge1x - sx * edge1z;
764 double qz = sx * edge1y - sy * edge1x;
765
766 double v = f * (dirX * qx + dirY * qy + dirZ * qz);
767 if (v < -1e-7 || u + v > 1.0000001)
768 continue;
769
770 double t = f * (edge2x * qx + edge2y * qy + edge2z * qz);
771 if (t > 1e-7 && t < closestDist) {
772 // Check barycentric coordinates with Fixed Relative Tolerance (25%)
773 // Relative tolerance scales with triangle size:
774 // - Large triangles (Helmet): Tolerates large gaps (~cm scale)
775 // - Small triangles (Brain): Tolerates small errors (~mm scale)
776 // This prevents clicking 'through' sparse meshes while staying precise on dense ones.
777 constexpr double tol = 0.25;
778
779 if (u >= -tol && v >= -tol && u + v <= 1.0 + tol) {
780 closestDist = t;
781 hit = true;
782
783 // Find closest vertex of the hit triangle to the hit point
784 // (Used for region lookup)
785 double hitX = originX + t * dirX;
786 double hitY = originY + t * dirY;
787 double hitZ = originZ + t * dirZ;
788
789 double d0 = (v0x - hitX) * (v0x - hitX) + (v0y - hitY) * (v0y - hitY) + (v0z - hitZ) * (v0z - hitZ);
790 double d1 = (v1x - hitX) * (v1x - hitX) + (v1y - hitY) * (v1y - hitY) + (v1z - hitZ) * (v1z - hitZ);
791 double d2 = (v2x - hitX) * (v2x - hitX) + (v2y - hitY) * (v2y - hitY) + (v2z - hitZ) * (v2z - hitZ);
792
793 if (d0 < d1 && d0 < d2)
794 closestVert = i0;
795 else if (d1 < d2)
796 closestVert = i1;
797 else
798 closestVert = i2;
799 }
800 }
801 }
802
803 if (hit) {
804 dist = static_cast<float>(closestDist);
805 vertexIdx = closestVert;
806 return true;
807 }
808
809 return false;
810}
811
812//=============================================================================================================
813
814QString BrainSurface::getAnnotationLabel(int vertexIdx) const
815{
816 if (!m_hasAnnotation || vertexIdx < 0 || vertexIdx >= m_vertexData.size()) {
817 return "";
818 }
819
820 const Eigen::VectorXi& vertices = m_annotation.getVertices();
821 const Eigen::VectorXi& labelIds = m_annotation.getLabelIds();
822 const FSLIB::FsColortable& ct = m_annotation.getColortable();
823
824 // The .annot file might not contain all vertices if it's sparse,
825 // but usually it contains a mapping for all.
826 // Let's find the labelId for this vertex.
827 int labelId = -1;
828 for (int i = 0; i < vertices.rows(); ++i) {
829 if (vertices(i) == vertexIdx) {
830 labelId = labelIds(i);
831 break;
832 }
833 }
834
835 if (labelId == -1)
836 return "Unknown";
837
838 // Find the name in colortable
839 for (int i = 0; i < ct.numEntries; ++i) {
840 if (ct.table(i, 4) == labelId) {
841 QString name = ct.struct_names[i];
842 // Remove null characters and trailing whitespace that might cause "strange signs"
843 while (!name.isEmpty() && (name.endsWith('\0') || name.endsWith(' '))) {
844 name.chop(1);
845 }
846 return name;
847 }
848 }
849
850 return "Unknown";
851}
852
854{
855 if (!m_hasAnnotation || vertexIdx < 0)
856 return -1;
857
858 const Eigen::VectorXi& vertices = m_annotation.getVertices();
859 const Eigen::VectorXi& labelIds = m_annotation.getLabelIds();
860
861 for (int i = 0; i < vertices.rows(); ++i) {
862 if (vertices(i) == vertexIdx) {
863 return labelIds(i);
864 }
865 }
866
867 return -1;
868}
869
871{
872 m_selectedRegionId = regionId;
873 updateVertexColors();
874}
875
876void BrainSurface::setSelected(bool selected)
877{
878 m_selected = selected;
879 updateVertexColors();
880}
881
882void BrainSurface::setSelectedVertexRange(int start, int count)
883{
884 m_selectedVertexStart = start;
885 m_selectedVertexCount = count;
886 updateVertexColors();
887}
888
889} // namespace DISP3DLIB
Renderable cortical / BEM mesh with interleaved vertex attributes and Qt-RHI buffer management.
3-D brain visualisation using the Qt RHI rendering backend.
uint32_t packABGR(uint32_t r, uint32_t g, uint32_t b, uint32_t a=0xFF)
Definition rendertypes.h:51
std::unique_ptr< QRhiBuffer > vertexBuffer
std::unique_ptr< QRhiBuffer > indexBuffer
Interleaved vertex attributes (position, normal, color, curvature) for brain surface GPU upload.
int getAnnotationLabelId(int vertexIdx) const
void setVisible(bool visible)
Eigen::MatrixX3f vertexPositions() const
Eigen::MatrixX3f verticesAsMatrix() const
void setSelectedVertexRange(int start, int count)
QRhiBuffer * indexBuffer() const
void fromSurface(const FSLIB::FsSurface &surf)
Eigen::MatrixX3f vertexNormals() const
void applyTransform(const QMatrix4x4 &m)
bool intersects(const QVector3D &rayOrigin, const QVector3D &rayDir, float &dist, int &vertexIdx) const
void setColor(const QColor &color)
QString getAnnotationLabel(int vertexIdx) const
static constexpr VisualizationMode ModeSurface
DISP3DLIB::VisualizationMode VisualizationMode
bool loadAnnotation(const QString &path)
void boundingBox(QVector3D &min, QVector3D &max) const
void transform(const QMatrix4x4 &m)
void fromBemSurface(const MNELIB::MNEBemSurface &surf, const QColor &color=Qt::white)
void setUseDefaultColor(bool useDefault)
void setSelectedRegion(int regionId)
void addAnnotation(const FSLIB::FsAnnotation &annotation)
void applySourceEstimateColors(const QVector< uint32_t > &colors)
QRhiBuffer * vertexBuffer() const
static constexpr VisualizationMode ModeSourceEstimate
void translateX(float offset)
std::vector< Eigen::VectorXi > computeNeighbors() const
void setSelected(bool selected)
void createFromData(const Eigen::MatrixX3f &vertices, const Eigen::MatrixX3i &triangles, const QColor &color)
void updateBuffers(QRhi *rhi, QRhiResourceUpdateBatch *u)
void setVisualizationMode(VisualizationMode mode)
Single-hemisphere FreeSurfer parcellation: vertex → region label plus embedded colortable.
static bool read(const QString &subject_id, qint32 hemi, const QString &atlas, const QString &subjects_dir, FsAnnotation &p_Annotation)
FreeSurfer colour lookup table: region name + RGBA + packed label, indexed by entry.
QStringList struct_names
Eigen::VectorXi getLabelIds() const
Eigen::MatrixXi table
In-memory FreeSurfer triangular cortical surface for one hemisphere.
Definition fs_surface.h:94
const Eigen::MatrixX3f & nn() const
Definition fs_surface.h:392
const Eigen::MatrixX3i & tris() const
Definition fs_surface.h:385
const Eigen::MatrixX3f & rr() const
Definition fs_surface.h:378
const Eigen::VectorXf & curv() const
Definition fs_surface.h:399
static Eigen::MatrixX3f compute_normals(const Eigen::MatrixX3f &rr, const Eigen::MatrixX3i &tris)
BEM surface provides geometry information.