v2.0.0
Loading...
Searching...
No Matches
networkobject.cpp
Go to the documentation of this file.
1//=============================================================================================================
12
13//=============================================================================================================
14// INCLUDES
15//=============================================================================================================
16
17#include "networkobject.h"
18
19#include <rhi/qrhi.h>
20
24
25#include <QQuaternion>
26#include <QDebug>
27#include <cmath>
28
29using namespace CONNECTIVITYLIB;
30using namespace DISPLIB;
31using namespace Eigen;
32
33//=============================================================================================================
34// PIMPL
35//=============================================================================================================
36
38{
39 // Node buffers
40 std::unique_ptr<QRhiBuffer> nodeVertexBuffer;
41 std::unique_ptr<QRhiBuffer> nodeIndexBuffer;
42 std::unique_ptr<QRhiBuffer> nodeInstanceBuffer;
43 // Edge buffers
44 std::unique_ptr<QRhiBuffer> edgeVertexBuffer;
45 std::unique_ptr<QRhiBuffer> edgeIndexBuffer;
46 std::unique_ptr<QRhiBuffer> edgeInstanceBuffer;
47};
48
49//=============================================================================================================
50// DEFINE MEMBER METHODS
51//=============================================================================================================
52
54 : m_gpu(std::make_unique<GpuBuffers>())
55{
56}
57
58//=============================================================================================================
59
61
62//=============================================================================================================
63
64QRhiBuffer* NetworkObject::nodeVertexBuffer() const { return m_gpu->nodeVertexBuffer.get(); }
65QRhiBuffer* NetworkObject::nodeIndexBuffer() const { return m_gpu->nodeIndexBuffer.get(); }
66QRhiBuffer* NetworkObject::nodeInstanceBuffer() const { return m_gpu->nodeInstanceBuffer.get(); }
67QRhiBuffer* NetworkObject::edgeVertexBuffer() const { return m_gpu->edgeVertexBuffer.get(); }
68QRhiBuffer* NetworkObject::edgeIndexBuffer() const { return m_gpu->edgeIndexBuffer.get(); }
69QRhiBuffer* NetworkObject::edgeInstanceBuffer() const { return m_gpu->edgeInstanceBuffer.get(); }
70
71//=============================================================================================================
72
73void NetworkObject::load(const Network &network, const QString &sColormap)
74{
75 m_network = network;
76 m_colormap = sColormap;
77
78 createNodeGeometry();
79 createEdgeGeometry();
80 buildNodeInstances();
81 buildEdgeInstances();
82}
83
84//=============================================================================================================
85
86void NetworkObject::setThreshold(double dThreshold)
87{
88 m_network.setThreshold(dThreshold);
89 buildNodeInstances();
90 buildEdgeInstances();
91}
92
93//=============================================================================================================
94
95void NetworkObject::setColormap(const QString &sColormap)
96{
97 m_colormap = sColormap;
98 buildNodeInstances();
99 buildEdgeInstances();
100}
101
102//=============================================================================================================
103
104void NetworkObject::createNodeGeometry()
105{
106 if (!m_nodeVertexData.isEmpty()) return;
107
108 // Create an icosphere (subdivision level 1) for nodes
109 const int subdivisions = 1;
110 const float radius = 1.0f; // Unit sphere, scaled per-instance
111
112 // Start with icosahedron
113 const float t = (1.0f + std::sqrt(5.0f)) / 2.0f;
114
115 std::vector<QVector3D> vertices = {
116 QVector3D(-1, t, 0).normalized() * radius,
117 QVector3D( 1, t, 0).normalized() * radius,
118 QVector3D(-1, -t, 0).normalized() * radius,
119 QVector3D( 1, -t, 0).normalized() * radius,
120 QVector3D( 0, -1, t).normalized() * radius,
121 QVector3D( 0, 1, t).normalized() * radius,
122 QVector3D( 0, -1, -t).normalized() * radius,
123 QVector3D( 0, 1, -t).normalized() * radius,
124 QVector3D( t, 0, -1).normalized() * radius,
125 QVector3D( t, 0, 1).normalized() * radius,
126 QVector3D(-t, 0, -1).normalized() * radius,
127 QVector3D(-t, 0, 1).normalized() * radius,
128 };
129
130 std::vector<uint32_t> indices = {
131 0,11,5, 0,5,1, 0,1,7, 0,7,10, 0,10,11,
132 1,5,9, 5,11,4, 11,10,2, 10,7,6, 7,1,8,
133 3,9,4, 3,4,2, 3,2,6, 3,6,8, 3,8,9,
134 4,9,5, 2,4,11, 6,2,10, 8,6,7, 9,8,1,
135 };
136
137 // Subdivide
138 for (int s = 0; s < subdivisions; ++s) {
139 std::vector<uint32_t> newIndices;
140 std::map<uint64_t, uint32_t> midpointCache;
141
142 auto getMidpoint = [&](uint32_t i0, uint32_t i1) -> uint32_t {
143 uint64_t key = (uint64_t)std::min(i0, i1) << 32 | std::max(i0, i1);
144 auto it = midpointCache.find(key);
145 if (it != midpointCache.end()) return it->second;
146
147 QVector3D mid = ((vertices[i0] + vertices[i1]) / 2.0f).normalized() * radius;
148 uint32_t idx = (uint32_t)vertices.size();
149 vertices.push_back(mid);
150 midpointCache[key] = idx;
151 return idx;
152 };
153
154 for (size_t i = 0; i < indices.size(); i += 3) {
155 uint32_t a = indices[i], b = indices[i + 1], c = indices[i + 2];
156 uint32_t ab = getMidpoint(a, b);
157 uint32_t bc = getMidpoint(b, c);
158 uint32_t ca = getMidpoint(c, a);
159
160 newIndices.insert(newIndices.end(), {a, ab, ca});
161 newIndices.insert(newIndices.end(), {b, bc, ab});
162 newIndices.insert(newIndices.end(), {c, ca, bc});
163 newIndices.insert(newIndices.end(), {ab, bc, ca});
164 }
165
166 indices = std::move(newIndices);
167 }
168
169 // Build vertex data with normals (normal = normalized position for sphere)
170 std::vector<VertexData> vd;
171 vd.reserve(vertices.size());
172 for (const auto &v : vertices) {
173 QVector3D n = v.normalized();
174 vd.push_back({v.x(), v.y(), v.z(), n.x(), n.y(), n.z()});
175 }
176
177 m_nodeIndexCount = (int)indices.size();
178
179 m_nodeVertexData.resize(vd.size() * sizeof(VertexData));
180 memcpy(m_nodeVertexData.data(), vd.data(), m_nodeVertexData.size());
181
182 m_nodeIndexData.resize(indices.size() * sizeof(uint32_t));
183 memcpy(m_nodeIndexData.data(), indices.data(), m_nodeIndexData.size());
184
185 m_nodeGeometryDirty = true;
186}
187
188//=============================================================================================================
189
190void NetworkObject::createEdgeGeometry()
191{
192 if (!m_edgeVertexData.isEmpty()) return;
193
194 // Create a unit cylinder along Y axis (height=1, radius=1, scaled per-instance)
195 const int segments = 8;
196 const float radius = 1.0f;
197 const float halfHeight = 0.5f;
198
199 std::vector<VertexData> vertices;
200 std::vector<uint32_t> indices;
201
202 // Top center (0)
203 vertices.push_back({0, halfHeight, 0, 0, 1, 0});
204 // Bottom center (1)
205 vertices.push_back({0, -halfHeight, 0, 0, -1, 0});
206
207 // Side vertices: top ring (2..2+segments-1), bottom ring (2+segments..2+2*segments-1)
208 for (int i = 0; i < segments; ++i) {
209 float angle = 2.0f * (float)M_PI * i / segments;
210 float x = radius * std::cos(angle);
211 float z = radius * std::sin(angle);
212
213 QVector3D normal(x, 0, z);
214 normal.normalize();
215
216 // Top side vertex
217 vertices.push_back({x, halfHeight, z, normal.x(), normal.y(), normal.z()});
218 // Bottom side vertex
219 vertices.push_back({x, -halfHeight, z, normal.x(), normal.y(), normal.z()});
220 }
221
222 // Top cap vertices (for proper normals)
223 int topCapStart = (int)vertices.size();
224 for (int i = 0; i < segments; ++i) {
225 float angle = 2.0f * (float)M_PI * i / segments;
226 float x = radius * std::cos(angle);
227 float z = radius * std::sin(angle);
228 vertices.push_back({x, halfHeight, z, 0, 1, 0});
229 }
230
231 // Bottom cap vertices
232 int botCapStart = (int)vertices.size();
233 for (int i = 0; i < segments; ++i) {
234 float angle = 2.0f * (float)M_PI * i / segments;
235 float x = radius * std::cos(angle);
236 float z = radius * std::sin(angle);
237 vertices.push_back({x, -halfHeight, z, 0, -1, 0});
238 }
239
240 // Side faces
241 int sideStart = 2;
242 for (int i = 0; i < segments; ++i) {
243 int next = (i + 1) % segments;
244 int topCur = sideStart + i * 2;
245 int botCur = sideStart + i * 2 + 1;
246 int topNext = sideStart + next * 2;
247 int botNext = sideStart + next * 2 + 1;
248
249 indices.insert(indices.end(), {(uint32_t)topCur, (uint32_t)topNext, (uint32_t)botCur});
250 indices.insert(indices.end(), {(uint32_t)botCur, (uint32_t)topNext, (uint32_t)botNext});
251 }
252
253 // Top cap
254 for (int i = 0; i < segments; ++i) {
255 int next = (i + 1) % segments;
256 indices.push_back(0); // center
257 indices.push_back(topCapStart + i);
258 indices.push_back(topCapStart + next);
259 }
260
261 // Bottom cap
262 for (int i = 0; i < segments; ++i) {
263 int next = (i + 1) % segments;
264 indices.push_back(1); // center
265 indices.push_back(botCapStart + next);
266 indices.push_back(botCapStart + i);
267 }
268
269 m_edgeIndexCount = (int)indices.size();
270
271 m_edgeVertexData.resize(vertices.size() * sizeof(VertexData));
272 memcpy(m_edgeVertexData.data(), vertices.data(), m_edgeVertexData.size());
273
274 m_edgeIndexData.resize(indices.size() * sizeof(uint32_t));
275 memcpy(m_edgeIndexData.data(), indices.data(), m_edgeIndexData.size());
276
277 m_edgeGeometryDirty = true;
278}
279
280//=============================================================================================================
281
282void NetworkObject::buildNodeInstances()
283{
284 if (m_network.isEmpty()) {
285 m_nodeInstanceCount = 0;
286 m_nodeInstancesDirty = true;
287 return;
288 }
289
290 const auto &nodes = m_network.getNodes();
291 qint16 iMaxDegree = m_network.getMinMaxThresholdedDegrees().second;
292 if (iMaxDegree == 0) iMaxDegree = 1;
293
294 VisualizationInfo vizInfo = m_network.getVisualizationInfo();
295
296 std::vector<InstanceData> instances;
297 instances.reserve(nodes.size());
298
299 for (int i = 0; i < nodes.size(); ++i) {
300 qint16 degree = nodes[i]->getThresholdedDegree();
301 if (degree == 0) continue;
302
303 const RowVectorXf &vert = nodes[i]->getVert();
304 QVector3D pos(vert(0), vert(1), vert(2));
305
306 // Scale: nodes with higher degree are larger
307 // Range: 0.0006 to 0.005 (same as disp3D)
308 float scaleFactor = ((float)degree / (float)iMaxDegree) * (0.005f - 0.0006f) + 0.0006f;
309
310 QMatrix4x4 m;
311 m.translate(pos);
312 m.scale(scaleFactor);
313
314 InstanceData inst;
315 const float *mPtr = m.constData();
316 for (int j = 0; j < 16; ++j) inst.model[j] = mPtr[j];
317
318 // Color: colormap-based or fixed
319 if (vizInfo.sMethod == "Map") {
320 float normalized = (float)degree / (float)iMaxDegree;
321 QRgb rgb = ColorMap::valueToColor(normalized, vizInfo.sColormap.isEmpty() ? m_colormap : vizInfo.sColormap);
322 QColor color(rgb);
323 float alpha = std::pow(normalized, 4.0f); // Same as disp3D
324 inst.color[0] = color.redF();
325 inst.color[1] = color.greenF();
326 inst.color[2] = color.blueF();
327 inst.color[3] = alpha;
328 } else {
329 inst.color[0] = vizInfo.colNodes[0] / 255.0f;
330 inst.color[1] = vizInfo.colNodes[1] / 255.0f;
331 inst.color[2] = vizInfo.colNodes[2] / 255.0f;
332 inst.color[3] = vizInfo.colNodes[3] / 255.0f;
333 }
334 inst.isSelected = 0.0f;
335
336 instances.push_back(inst);
337 }
338
339 m_nodeInstanceCount = (int)instances.size();
340 m_nodeInstanceData.resize(m_nodeInstanceCount * sizeof(InstanceData));
341 if (m_nodeInstanceCount > 0) {
342 memcpy(m_nodeInstanceData.data(), instances.data(), m_nodeInstanceData.size());
343 }
344 m_nodeInstancesDirty = true;
345
346 qDebug() << "NetworkObject: Built" << m_nodeInstanceCount << "node instances";
347}
348
349//=============================================================================================================
350
351void NetworkObject::buildEdgeInstances()
352{
353 if (m_network.isEmpty()) {
354 m_edgeInstanceCount = 0;
355 m_edgeInstancesDirty = true;
356 return;
357 }
358
359 const auto &edges = m_network.getThresholdedEdges();
360 const auto &nodes = m_network.getNodes();
361
362 double dMaxWeight = m_network.getMinMaxThresholdedWeights().second;
363 double dMinWeight = m_network.getMinMaxThresholdedWeights().first;
364 double dWeightRange = dMaxWeight - dMinWeight;
365 if (dWeightRange == 0.0) dWeightRange = 1.0;
366
367 VisualizationInfo vizInfo = m_network.getVisualizationInfo();
368
369 std::vector<InstanceData> instances;
370 instances.reserve(edges.size());
371
372 for (int i = 0; i < edges.size(); ++i) {
373 auto &edge = edges[i];
374 if (!edge->isActive()) continue;
375
376 int iStart = edge->getStartNodeID();
377 int iEnd = edge->getEndNodeID();
378
379 if (iStart < 0 || iStart >= nodes.size() || iEnd < 0 || iEnd >= nodes.size()) continue;
380
381 const RowVectorXf &vStart = nodes[iStart]->getVert();
382 const RowVectorXf &vEnd = nodes[iEnd]->getVert();
383
384 QVector3D startPos(vStart(0), vStart(1), vStart(2));
385 QVector3D endPos(vEnd(0), vEnd(1), vEnd(2));
386
387 if (startPos == endPos) continue;
388
389 double dWeight = std::fabs(edge->getWeight());
390 if (dWeight == 0.0) continue;
391
392 QVector3D diff = endPos - startPos;
393 QVector3D midPoint = startPos + diff / 2.0f;
394 float length = diff.length();
395
396 // Build transform: translate to midpoint, rotate Y-axis to diff direction, scale
397 float normalizedWeight = (float)std::fabs((dWeight - dMinWeight) / dWeightRange);
398
399 // Cylinder radius: proportional to weight, range 0.0001 to 0.001
400 float edgeRadius = 0.0001f + normalizedWeight * 0.0009f;
401
402 QMatrix4x4 m;
403 m.translate(midPoint);
404 m.rotate(QQuaternion::rotationTo(QVector3D(0, 1, 0), diff.normalized()));
405 m.scale(edgeRadius, length, edgeRadius);
406
407 InstanceData inst;
408 const float *mPtr = m.constData();
409 for (int j = 0; j < 16; ++j) inst.model[j] = mPtr[j];
410
411 // Color
412 if (vizInfo.sMethod == "Map") {
413 float normalized = (dMaxWeight != 0.0) ? (float)std::fabs(dWeight / dMaxWeight) : 0.0f;
414 QRgb rgb = ColorMap::valueToColor(normalized, vizInfo.sColormap.isEmpty() ? m_colormap : vizInfo.sColormap);
415 QColor color(rgb);
416 float alpha = std::pow(normalized, 1.5f); // Same as disp3D
417 inst.color[0] = color.redF();
418 inst.color[1] = color.greenF();
419 inst.color[2] = color.blueF();
420 inst.color[3] = alpha;
421 } else {
422 inst.color[0] = vizInfo.colEdges[0] / 255.0f;
423 inst.color[1] = vizInfo.colEdges[1] / 255.0f;
424 inst.color[2] = vizInfo.colEdges[2] / 255.0f;
425 inst.color[3] = vizInfo.colEdges[3] / 255.0f;
426 }
427 inst.isSelected = 0.0f;
428
429 instances.push_back(inst);
430 }
431
432 m_edgeInstanceCount = (int)instances.size();
433 m_edgeInstanceData.resize(m_edgeInstanceCount * sizeof(InstanceData));
434 if (m_edgeInstanceCount > 0) {
435 memcpy(m_edgeInstanceData.data(), instances.data(), m_edgeInstanceData.size());
436 }
437 m_edgeInstancesDirty = true;
438
439 qDebug() << "NetworkObject: Built" << m_edgeInstanceCount << "edge instances";
440}
441
442//=============================================================================================================
443
444void NetworkObject::updateNodeBuffers(QRhi *rhi, QRhiResourceUpdateBatch *u)
445{
446 if (m_nodeGeometryDirty) {
447 if (!m_gpu->nodeVertexBuffer) {
448 m_gpu->nodeVertexBuffer.reset(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, m_nodeVertexData.size()));
449 m_gpu->nodeVertexBuffer->create();
450 }
451 if (!m_gpu->nodeIndexBuffer) {
452 m_gpu->nodeIndexBuffer.reset(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, m_nodeIndexData.size()));
453 m_gpu->nodeIndexBuffer->create();
454 }
455 u->uploadStaticBuffer(m_gpu->nodeVertexBuffer.get(), m_nodeVertexData.constData());
456 u->uploadStaticBuffer(m_gpu->nodeIndexBuffer.get(), m_nodeIndexData.constData());
457 m_nodeGeometryDirty = false;
458 }
459
460 if (m_nodeInstancesDirty && m_nodeInstanceCount > 0) {
461 int requiredSize = m_nodeInstanceData.size();
462 if (!m_gpu->nodeInstanceBuffer || m_gpu->nodeInstanceBuffer->size() < requiredSize) {
463 m_gpu->nodeInstanceBuffer.reset(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::VertexBuffer, requiredSize));
464 m_gpu->nodeInstanceBuffer->create();
465 }
466 u->updateDynamicBuffer(m_gpu->nodeInstanceBuffer.get(), 0, requiredSize, m_nodeInstanceData.constData());
467 m_nodeInstancesDirty = false;
468 }
469}
470
471//=============================================================================================================
472
473void NetworkObject::updateEdgeBuffers(QRhi *rhi, QRhiResourceUpdateBatch *u)
474{
475 if (m_edgeGeometryDirty) {
476 if (!m_gpu->edgeVertexBuffer) {
477 m_gpu->edgeVertexBuffer.reset(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, m_edgeVertexData.size()));
478 m_gpu->edgeVertexBuffer->create();
479 }
480 if (!m_gpu->edgeIndexBuffer) {
481 m_gpu->edgeIndexBuffer.reset(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, m_edgeIndexData.size()));
482 m_gpu->edgeIndexBuffer->create();
483 }
484 u->uploadStaticBuffer(m_gpu->edgeVertexBuffer.get(), m_edgeVertexData.constData());
485 u->uploadStaticBuffer(m_gpu->edgeIndexBuffer.get(), m_edgeIndexData.constData());
486 m_edgeGeometryDirty = false;
487 }
488
489 if (m_edgeInstancesDirty && m_edgeInstanceCount > 0) {
490 int requiredSize = m_edgeInstanceData.size();
491 if (!m_gpu->edgeInstanceBuffer || m_gpu->edgeInstanceBuffer->size() < requiredSize) {
492 m_gpu->edgeInstanceBuffer.reset(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::VertexBuffer, requiredSize));
493 m_gpu->edgeInstanceBuffer->create();
494 }
495 u->updateDynamicBuffer(m_gpu->edgeInstanceBuffer.get(), 0, requiredSize, m_edgeInstanceData.constData());
496 m_edgeInstancesDirty = false;
497 }
498}
Instanced connectivity-graph renderable: node spheres and edge cylinders coloured by weight through a...
#define M_PI
Static scalar-to-colour lookup helpers (Jet, Hot, Bone, Viridis, Cool, RedBlue, MNE) used by every pl...
Weighted edge between two NetworkNode instances; stores the full per-frequency weight matrix and the ...
Node of a connectivity Network; carries a 3D position and the lists of incident (in / out,...
Functional connectivity metrics (coherence, PLV, cross-correlation, etc.).
2-D display widgets and visualisation helpers (charts, topography, colour maps).
Graph container for one connectivity metric; nodes + weighted edges + threshold/visualisation state.
Definition network.h:98
static QRgb valueToColor(double v, const QString &sMap)
Definition colormap.h:681
std::unique_ptr< QRhiBuffer > nodeVertexBuffer
std::unique_ptr< QRhiBuffer > edgeInstanceBuffer
std::unique_ptr< QRhiBuffer > nodeInstanceBuffer
std::unique_ptr< QRhiBuffer > nodeIndexBuffer
std::unique_ptr< QRhiBuffer > edgeVertexBuffer
std::unique_ptr< QRhiBuffer > edgeIndexBuffer
QRhiBuffer * nodeIndexBuffer() const
void setColormap(const QString &sColormap)
void updateNodeBuffers(QRhi *rhi, QRhiResourceUpdateBatch *u)
QRhiBuffer * edgeIndexBuffer() const
void setThreshold(double dThreshold)
void load(const CONNECTIVITYLIB::Network &network, const QString &sColormap="Viridis")
QRhiBuffer * nodeInstanceBuffer() const
QRhiBuffer * edgeVertexBuffer() const
QRhiBuffer * nodeVertexBuffer() const
void updateEdgeBuffers(QRhi *rhi, QRhiResourceUpdateBatch *u)
QRhiBuffer * edgeInstanceBuffer() const