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