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