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