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