v2.0.0
Loading...
Searching...
No Matches
dipoleobject.cpp
Go to the documentation of this file.
1//=============================================================================================================
12
13//=============================================================================================================
14// INCLUDES
15//=============================================================================================================
16
17#include "dipoleobject.h"
18
19#include <rhi/qrhi.h>
20#include <cmath>
21#include <QQuaternion>
22#include <QRandomGenerator>
23#include <QDebug>
24
25//=============================================================================================================
26// PIMPL
27//=============================================================================================================
28
30{
31 std::unique_ptr<QRhiBuffer> vertexBuffer;
32 std::unique_ptr<QRhiBuffer> indexBuffer;
33 std::unique_ptr<QRhiBuffer> instanceBuffer;
34};
35
37 : m_gpu(std::make_unique<GpuBuffers>())
38{
39}
40
42
43QRhiBuffer* DipoleObject::vertexBuffer() const { return m_gpu->vertexBuffer.get(); }
44QRhiBuffer* DipoleObject::indexBuffer() const { return m_gpu->indexBuffer.get(); }
45QRhiBuffer* DipoleObject::instanceBuffer() const { return m_gpu->instanceBuffer.get(); }
46
48{
49 createGeometry();
50
51 m_instanceCount = ecdSet.size();
52 m_instanceData.resize(m_instanceCount * sizeof(InstanceData));
53 InstanceData *data = reinterpret_cast<InstanceData*>(m_instanceData.data());
54
55 QVector3D from(0.0f, 1.0f, 0.0f); // Cone points up Y axis
56
57 if (ecdSet.size() > 0) {
58 qDebug() << "DipoleObject: First dipole raw pos:" << ecdSet[0].rd(0) << ecdSet[0].rd(1) << ecdSet[0].rd(2);
59 }
60
61 // Heuristic: Check if coordinates are likely in mm (e.g., > 1.0 or similar)
62 // Head model is usually < 0.2m radius. If values are > 0.5, they are likely mm or cm.
63 // sample data is ~44.56 mm.
64 float unitScale = 1.0f;
65 float maxCoord = 0.0f;
66 float maxMag = 0.0f;
67
68 for(int i=0; i < ecdSet.size(); ++i) {
69 maxCoord = std::max(maxCoord, std::abs(ecdSet[i].rd(0)));
70 maxCoord = std::max(maxCoord, std::abs(ecdSet[i].rd(1)));
71 maxCoord = std::max(maxCoord, std::abs(ecdSet[i].rd(2)));
72
73 float mag = std::sqrt(std::pow(ecdSet[i].Q(0), 2) + std::pow(ecdSet[i].Q(1), 2) + std::pow(ecdSet[i].Q(2), 2));
74 maxMag = std::max(maxMag, mag);
75 }
76
77 if (maxCoord > 5.0f) {
78 unitScale = 0.001f; // Convert mm to meters
79 qDebug() << "DipoleObject: Detected large coordinates (max" << maxCoord << "), applying mm->m scale (0.001).";
80 }
81
82 qDebug() << "DipoleObject: Loading" << m_instanceCount << "dipoles with scale" << unitScale;
83
84 for (int i = 0; i < ecdSet.size(); ++i) {
85 const auto &dip = ecdSet[i];
86
87 QVector3D pos(dip.rd(0) * unitScale, dip.rd(1) * unitScale, dip.rd(2) * unitScale);
88 QVector3D Q(dip.Q(0), dip.Q(1), dip.Q(2));
89 float mag = Q.length();
90
91 QVector3D to = Q.normalized();
92 // Handle case where 'to' is parallel to 'from'
93 QQuaternion rot;
94 if (QVector3D::dotProduct(from, to) > 0.99f) {
95 rot = QQuaternion();
96 } else if (QVector3D::dotProduct(from, to) < -0.99f) {
97 rot = QQuaternion::fromAxisAndAngle(1.0f, 0.0f, 0.0f, 180.0f);
98 } else {
99 rot = QQuaternion::rotationTo(from, to);
100 }
101
102 // Scale based on magnitude relative to max, with a minimum size of 20%
103 float scaleFactor = (maxMag > 0.0f) ? (0.2f + 0.8f * (mag / maxMag)) : 1.0f;
104
105 QMatrix4x4 m;
106 m.translate(pos);
107 m.rotate(rot);
108 m.scale(scaleFactor);
109
110 const float *mPtr = m.constData();
111 for (int j = 0; j < 16; ++j) {
112 data[i].model[j] = mPtr[j];
113 }
114
115 // Random Color
116 data[i].color[0] = QRandomGenerator::global()->generateDouble();
117 data[i].color[1] = QRandomGenerator::global()->generateDouble();
118 data[i].color[2] = QRandomGenerator::global()->generateDouble();
119 data[i].color[3] = 1.0f;
120
121 // Selection state
122 data[i].isSelected = 0.0f;
123 }
124
125 if (m_instanceCount > 0) {
126 InstanceData *data = reinterpret_cast<InstanceData*>(m_instanceData.data());
127 float x = data[0].model[12];
128 float y = data[0].model[13];
129 float z = data[0].model[14];
130 qDebug() << "DipoleObject: First dipole initial pos (Scaled):" << x << y << z;
131 }
132
133 m_instancesDirty = true;
134}
135
136void DipoleObject::applyTransform(const QMatrix4x4 &trans)
137{
138 if (m_instanceCount == 0) return;
139
140 InstanceData *data = reinterpret_cast<InstanceData*>(m_instanceData.data());
141
142 for (int i = 0; i < m_instanceCount; ++i) {
143 // Reconstruct current model matrix
144 QMatrix4x4 currentModel;
145 const float *src = data[i].model;
146 float *dst = currentModel.data();
147 for(int j=0; j<16; ++j) dst[j] = src[j];
148
149 // Apply transformation: NewModel = Trans * OldModel
150 // Wait, OldModel places the cone at Pos with Rot.
151 // We want to transform Pos and Rot.
152 // So yes, Trans * OldModel works.
153
154 QMatrix4x4 newModel = trans * currentModel;
155
156 // Store back
157 const float *newPtr = newModel.constData();
158 for (int j = 0; j < 16; ++j) {
159 data[i].model[j] = newPtr[j];
160 }
161 }
162
163 if (m_instanceCount > 0) {
164 // Log first instance pos
165 InstanceData *data = reinterpret_cast<InstanceData*>(m_instanceData.data());
166 // Matrix is column major? No, we stored it as we got it from QMatrix4x4.constData(), which is Column-Major.
167 // Translation is in the last column (indices 12, 13, 14).
168 float x = data[0].model[12];
169 float y = data[0].model[13];
170 float z = data[0].model[14];
171 qDebug() << "DipoleObject: First dipole transformed pos (Meters):" << x << y << z;
172 }
173
174 m_instancesDirty = true;
175}
176
178{
179 if (m_instanceCount == 0) return QVector3D();
180 const InstanceData *data = reinterpret_cast<const InstanceData*>(m_instanceData.constData());
181 // Column 3 is translation (12, 13, 14)
182 return QVector3D(data[0].model[12], data[0].model[13], data[0].model[14]);
183}
184
185void DipoleObject::createGeometry()
186{
187 if (!m_vertexData.isEmpty()) return;
188
189 // Create a simple cone
190 // Radius 0.001, Height 0.003
191 // 32 segments
192
193 float radius = 0.005f; // Slightly larger for visibility
194 float height = 0.01f;
195 int segments = 16;
196
197 std::vector<VertexData> vertices;
198 std::vector<uint32_t> indices;
199
200 // Tip
201 VertexData tip = {0, height, 0, 0, 1, 0};
202 vertices.push_back(tip);
203
204 // Base center
205 VertexData baseCenter = {0, 0, 0, 0, -1, 0};
206 vertices.push_back(baseCenter); // Index 1
207
208 // Rim vertices
209 int centerIdx = 1;
210 int firstRimIdx = 2;
211
212 for (int i = 0; i < segments; ++i) {
213 float angle = 2.0f * M_PI * i / segments;
214 float x = radius * cos(angle);
215 float z = radius * sin(angle);
216
217 // Side normal
218 // Slope vector: (cos, -h/r, sin) -> normalized
219 QVector3D n(x, radius/height, z);
220 n.normalize();
221
222 VertexData vSide = {x, 0, z, n.x(), n.y(), n.z()};
223 vertices.push_back(vSide);
224
225 VertexData vBase = {x, 0, z, 0, -1, 0};
226 vertices.push_back(vBase);
227 }
228
229 // Indices
230 for (int i = 0; i < segments; ++i) {
231 int next = (i + 1) % segments;
232
233 // Cone sides
234 // Tip (0), current(i), next(next)
235 // Side vertices start at firstRimIdx.
236 // Layout: Tip, BaseCenter, Side0, Base0, Side1, Base1...
237 // Wait, easier to keep separate lists or just flat
238
239 // Re-do layout for ease:
240 // 0: Tip
241 // 1: Base Center
242 // 2..2+segments-1: Side rim vertices
243 // 2+segments..2+2*segments-1: Base rim vertices
244 }
245
246 vertices.clear();
247 vertices.push_back(tip); // 0
248 vertices.push_back(baseCenter); // 1
249
250 for (int i=0; i<segments; ++i) {
251 float angle = 2.0f * M_PI * i / segments;
252 float x = radius * cos(angle);
253 float z = radius * sin(angle);
254
255 QVector3D n(x, 0, z); // Approximate normal for side (flat shading effectively if we don't slant)
256 // Better normal: vector perpendicular to slope
257 QVector3D slope(x, -height, z);
258 QVector3D tangent(-sin(angle), 0, cos(angle));
259 QVector3D sideNormal = QVector3D::crossProduct(tangent, slope).normalized(); // Wait, slope is vector down side.
260 // Actually simple: normal y component is radius/height ratio related.
261
262 vertices.push_back({x, 0, z, sideNormal.x(), sideNormal.y(), sideNormal.z()}); // Side rim
263 }
264
265 for (int i=0; i<segments; ++i) {
266 float angle = 2.0f * M_PI * i / segments;
267 float x = radius * cos(angle);
268 float z = radius * sin(angle);
269 vertices.push_back({x, 0, z, 0, -1, 0}); // Base rim
270 }
271
272 int sideStart = 2;
273 int baseStart = 2 + segments;
274
275 for (int i = 0; i < segments; ++i) {
276 int next = (i + 1) % segments;
277
278 // Cone face
279 indices.push_back(0);
280 indices.push_back(sideStart + next);
281 indices.push_back(sideStart + i);
282
283 // Base face
284 indices.push_back(1);
285 indices.push_back(baseStart + i);
286 indices.push_back(baseStart + next);
287 }
288
289 m_indexCount = indices.size();
290
291 m_vertexData.resize(vertices.size() * sizeof(VertexData));
292 memcpy(m_vertexData.data(), vertices.data(), m_vertexData.size());
293
294 m_indexData.resize(indices.size() * sizeof(uint32_t));
295 memcpy(m_indexData.data(), indices.data(), m_indexData.size());
296
297 m_geometryDirty = true;
298}
299
300void DipoleObject::updateBuffers(QRhi *rhi, QRhiResourceUpdateBatch *u)
301{
302 if (m_geometryDirty) {
303 if (!m_gpu->vertexBuffer) {
304 m_gpu->vertexBuffer.reset(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, m_vertexData.size()));
305 m_gpu->vertexBuffer->create();
306 qDebug() << "DipoleObject: Created vertex buffer size" << m_vertexData.size();
307 }
308 if (!m_gpu->indexBuffer) {
309 m_gpu->indexBuffer.reset(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, m_indexData.size()));
310 m_gpu->indexBuffer->create();
311 qDebug() << "DipoleObject: Created index buffer size" << m_indexData.size();
312 }
313 u->uploadStaticBuffer(m_gpu->vertexBuffer.get(), m_vertexData.constData());
314 u->uploadStaticBuffer(m_gpu->indexBuffer.get(), m_indexData.constData());
315 m_geometryDirty = false;
316 }
317
318 if (m_instancesDirty && m_instanceCount > 0) {
319 if (!m_gpu->instanceBuffer || m_gpu->instanceBuffer->size() < m_instanceData.size()) {
320 m_gpu->instanceBuffer.reset(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::VertexBuffer, m_instanceData.size()));
321 m_gpu->instanceBuffer->create();
322 qDebug() << "DipoleObject: Created instance buffer size" << m_instanceData.size();
323 }
324 u->updateDynamicBuffer(m_gpu->instanceBuffer.get(), 0, m_instanceData.size(), m_instanceData.constData());
325 m_instancesDirty = false;
326 }
327}
328
329int DipoleObject::intersect(const QVector3D &rayOrigin, const QVector3D &rayDir, float &dist) const
330{
331 if (m_instanceCount == 0) return -1;
332
333 int closestIdx = -1;
334 float closestDist = std::numeric_limits<float>::max();
335
336 const InstanceData *data = reinterpret_cast<const InstanceData*>(m_instanceData.constData());
337
338 // Geometry radius ~ 0.005 (base) to 0.01 (height).
339 // Let's use a slightly larger hit radius to make selection easier and more accurate.
340 const float baseRadius = 0.02f;
341
342 for (int i = 0; i < m_instanceCount; ++i) {
343 // Extract translation (last column)
344 QVector3D center(data[i].model[12], data[i].model[13], data[i].model[14]);
345
346 // Extract scale (length of first column) - assumes uniform scale roughly
347 QVector3D col0(data[i].model[0], data[i].model[1], data[i].model[2]);
348 float scale = col0.length();
349
350 float radius = baseRadius * scale;
351
352 // Ray-Sphere Intersection
353 QVector3D L = center - rayOrigin;
354 float tca = QVector3D::dotProduct(L, rayDir);
355
356 if (tca < 0) continue; // Behind ray
357
358 float d2 = QVector3D::dotProduct(L, L) - tca * tca;
359 if (d2 > radius * radius) continue; // Miss
360
361 float thc = std::sqrt(radius * radius - d2);
362 float t0 = tca - thc;
363 float t1 = tca + thc;
364
365 float t = t0;
366 if (t < 0) t = t1;
367 if (t < 0) continue;
368
369 if (t < closestDist) {
370 closestDist = t;
371 closestIdx = i;
372 }
373 }
374
375 if (closestIdx != -1) {
376 dist = closestDist;
377 return closestIdx;
378 }
379
380 return -1;
381}
382
383void DipoleObject::setSelected(int index, bool selected)
384{
385 if (index < 0 || index >= m_instanceCount) return;
386
387 InstanceData *data = reinterpret_cast<InstanceData*>(m_instanceData.data());
388
389 data[index].isSelected = selected ? 1.0f : 0.0f;
390
391 m_instancesDirty = true;
392}
Instanced-arrow renderable for fitted equivalent current dipoles, driven by QRhi instancing.
#define M_PI
Interleaved vertex attributes (position, normal, color, curvature) for brain surface GPU upload.
std::unique_ptr< QRhiBuffer > instanceBuffer
std::unique_ptr< QRhiBuffer > vertexBuffer
std::unique_ptr< QRhiBuffer > indexBuffer
QRhiBuffer * instanceBuffer() const
int intersect(const QVector3D &rayOrigin, const QVector3D &rayDir, float &dist) const
QVector3D debugFirstDipolePosition() const
void updateBuffers(QRhi *rhi, QRhiResourceUpdateBatch *u)
void load(const INVLIB::InvEcdSet &ecdSet)
QRhiBuffer * indexBuffer() const
void setSelected(int index, bool selected)
QRhiBuffer * vertexBuffer() const
void applyTransform(const QMatrix4x4 &trans)
Holds a set of Electric Current Dipoles.
Definition inv_ecd_set.h:64
qint32 size() const