VisuTwin Canvas
C++ 3D Engine — Metal Backend
Loading...
Searching...
No Matches
picker.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2025-2026 Arnis Lektauers
3//
4//
5#include "picker.h"
6
7#include <algorithm>
8#include <array>
9#include <cmath>
10#include <limits>
11#include <unordered_set>
12
13#include "core/math/matrix4.h"
14#include "core/math/vector4.h"
15#include "core/shape/ray.h"
18#include "framework/engine.h"
19#include "framework/entity.h"
20#include "scene/meshInstance.h"
21
22namespace visutwin::canvas
23{
24 Picker::Picker(Engine* app, const int width, const int height, const bool depth)
25 : _app(app), _depth(depth)
26 {
28 }
29
30 void Picker::resize(const int width, const int height)
31 {
32 _width = std::max(1, width);
33 _height = std::max(1, height);
34 }
35
36 void Picker::prepare(CameraComponent* camera, Scene* scene, const std::vector<int>& layers)
37 {
38 _camera = camera;
39 _scene = scene;
40 _layers = layers.empty() ? (_camera ? _camera->layers() : std::vector<int>{}) : layers;
41 _candidates.clear();
42 _candidateIndex.clear();
43
44 if (!_camera || !_camera->camera() || !_camera->entity()) {
45 return;
46 }
47
48 const Vector3 cameraPos = _camera->entity()->position();
49
50 for (auto* renderComponent : RenderComponent::instances()) {
51 if (!renderComponent || !renderComponent->enabled()) {
52 continue;
53 }
54
55 if (!isLayerAllowed(renderComponent->layers())) {
56 continue;
57 }
58
59 for (auto* meshInstance : renderComponent->meshInstances()) {
60 if (!meshInstance || !meshInstance->node()) {
61 continue;
62 }
63
64 const BoundingBox aabb = meshInstance->aabb();
65 const Vector3 center = aabb.center();
66 const Vector3 half = aabb.halfExtents();
67
68 std::array<Vector3, 8> corners = {{
69 Vector3(center.getX() - half.getX(), center.getY() - half.getY(), center.getZ() - half.getZ()),
70 Vector3(center.getX() + half.getX(), center.getY() - half.getY(), center.getZ() - half.getZ()),
71 Vector3(center.getX() - half.getX(), center.getY() + half.getY(), center.getZ() - half.getZ()),
72 Vector3(center.getX() + half.getX(), center.getY() + half.getY(), center.getZ() - half.getZ()),
73 Vector3(center.getX() - half.getX(), center.getY() - half.getY(), center.getZ() + half.getZ()),
74 Vector3(center.getX() + half.getX(), center.getY() - half.getY(), center.getZ() + half.getZ()),
75 Vector3(center.getX() - half.getX(), center.getY() + half.getY(), center.getZ() + half.getZ()),
76 Vector3(center.getX() + half.getX(), center.getY() + half.getY(), center.getZ() + half.getZ())
77 }};
78
79 bool anyProjected = false;
80 float minX = std::numeric_limits<float>::max();
81 float minY = std::numeric_limits<float>::max();
82 float maxX = std::numeric_limits<float>::lowest();
83 float maxY = std::numeric_limits<float>::lowest();
84
85 for (const auto& corner : corners) {
86 float sx = 0.0f;
87 float sy = 0.0f;
88 if (!projectPoint(corner, sx, sy)) {
89 continue;
90 }
91 anyProjected = true;
92 minX = std::min(minX, sx);
93 minY = std::min(minY, sy);
94 maxX = std::max(maxX, sx);
95 maxY = std::max(maxY, sy);
96 }
97
98 if (!anyProjected) {
99 continue;
100 }
101
102 Candidate candidate;
103 candidate.meshInstance = meshInstance;
104 candidate.minX = minX;
105 candidate.minY = minY;
106 candidate.maxX = maxX;
107 candidate.maxY = maxY;
108 const Vector3 delta = center - cameraPos;
109 candidate.distanceSq = delta.lengthSquared();
110 candidate.bounds = BoundingSphere(center, std::max(half.length(), 0.001f));
111
112 _candidateIndex[candidate.meshInstance] = _candidates.size();
113 _candidates.push_back(candidate);
114 }
115 }
116 }
117
118 std::vector<MeshInstance*> Picker::getSelection(const int x, const int y, const int width, const int height) const
119 {
120 std::vector<MeshInstance*> selection;
121 if (!_camera) {
122 return selection;
123 }
124
125 const Rect rect = sanitizeRect(x, y, width, height);
126 const float rectMaxX = static_cast<float>(rect.x + rect.width);
127 const float rectMaxY = static_cast<float>(rect.y + rect.height);
128
129 std::unordered_set<MeshInstance*> seen;
130 for (const auto& candidate : _candidates) {
131 if (!candidate.meshInstance) {
132 continue;
133 }
134
135 if (candidate.maxX < static_cast<float>(rect.x) || candidate.minX > rectMaxX ||
136 candidate.maxY < static_cast<float>(rect.y) || candidate.minY > rectMaxY) {
137 continue;
138 }
139
140 if (seen.insert(candidate.meshInstance).second) {
141 selection.push_back(candidate.meshInstance);
142 }
143 }
144
145 std::sort(selection.begin(), selection.end(), [&](const MeshInstance* a, const MeshInstance* b) {
146 const auto ita = _candidateIndex.find(const_cast<MeshInstance*>(a));
147 const auto itb = _candidateIndex.find(const_cast<MeshInstance*>(b));
148 if (ita == _candidateIndex.end() || itb == _candidateIndex.end()) {
149 return a < b;
150 }
151 return _candidates[ita->second].distanceSq < _candidates[itb->second].distanceSq;
152 });
153
154 return selection;
155 }
156
157 MeshInstance* Picker::getSelectionSingle(const int x, const int y) const
158 {
159 const auto selection = getSelection(x, y, 1, 1);
160 return selection.empty() ? nullptr : selection.front();
161 }
162
163 std::optional<Vector3> Picker::getWorldPoint(const int x, const int y) const
164 {
165 if (!_depth || !_camera) {
166 return std::nullopt;
167 }
168
169 Vector3 rayOrigin;
170 Vector3 rayDirection;
171 if (!buildRay(x, y, rayOrigin, rayDirection)) {
172 return std::nullopt;
173 }
174
175 const auto selected = getSelection(x, y, 1, 1);
176 if (selected.empty()) {
177 return std::nullopt;
178 }
179
180 Ray ray(rayOrigin, rayDirection);
181 float nearestDistanceSq = std::numeric_limits<float>::max();
182 std::optional<Vector3> nearestPoint;
183
184 for (auto* meshInstance : selected) {
185 const auto it = _candidateIndex.find(meshInstance);
186 if (it == _candidateIndex.end()) {
187 continue;
188 }
189
190 Vector3 hitPoint;
191 if (!_candidates[it->second].bounds.intersectsRay(ray, &hitPoint)) {
192 continue;
193 }
194
195 const float hitDistanceSq = (hitPoint - rayOrigin).lengthSquared();
196 if (hitDistanceSq < nearestDistanceSq) {
197 nearestDistanceSq = hitDistanceSq;
198 nearestPoint = hitPoint;
199 }
200 }
201
202 return nearestPoint;
203 }
204
205 bool Picker::projectPoint(const Vector3& worldPos, float& outX, float& outY) const
206 {
207 if (!_camera || !_camera->camera() || !_camera->entity()) {
208 return false;
209 }
210
211 const Matrix4 viewMatrix = _camera->entity()->worldTransform().inverse();
212 const Matrix4& projMatrix = _camera->camera()->projectionMatrix();
213
214 const Vector3 viewPos = viewMatrix.transformPoint(worldPos);
215 if (viewPos.getZ() >= 0.0f) {
216 return false;
217 }
218
219 const Vector4 clipPos = projMatrix * Vector4(viewPos.getX(), viewPos.getY(), viewPos.getZ(), 1.0f);
220 if (std::abs(clipPos.getW()) < 1e-6f) {
221 return false;
222 }
223
224 const float ndcX = clipPos.getX() / clipPos.getW();
225 const float ndcY = clipPos.getY() / clipPos.getW();
226
227 outX = (ndcX * 0.5f + 0.5f) * static_cast<float>(_width);
228 outY = (1.0f - (ndcY * 0.5f + 0.5f)) * static_cast<float>(_height);
229 return true;
230 }
231
232 bool Picker::buildRay(const int x, const int y, Vector3& outOrigin, Vector3& outDirection) const
233 {
234 if (!_camera || !_camera->camera() || !_camera->entity()) {
235 return false;
236 }
237
238 const Rect rect = sanitizeRect(x, y, 1, 1);
239 const float px = static_cast<float>(rect.x);
240 const float py = static_cast<float>(rect.y);
241
242 const float ndcX = (px / static_cast<float>(_width)) * 2.0f - 1.0f;
243 const float ndcY = 1.0f - (py / static_cast<float>(_height)) * 2.0f;
244
245 const Matrix4 viewMatrix = _camera->entity()->worldTransform().inverse();
246 const Matrix4 viewProjection = _camera->camera()->projectionMatrix() * viewMatrix;
247 const Matrix4 invViewProjection = viewProjection.inverse();
248
249 Vector4 nearClip(ndcX, ndcY, 1.0f, 1.0f);
250 Vector4 farClip(ndcX, ndcY, 0.0f, 1.0f);
251
252 nearClip = invViewProjection * nearClip;
253 farClip = invViewProjection * farClip;
254
255 if (std::abs(nearClip.getW()) < 1e-6f || std::abs(farClip.getW()) < 1e-6f) {
256 return false;
257 }
258
259 const Vector3 nearWorld(
260 nearClip.getX() / nearClip.getW(),
261 nearClip.getY() / nearClip.getW(),
262 nearClip.getZ() / nearClip.getW()
263 );
264 const Vector3 farWorld(
265 farClip.getX() / farClip.getW(),
266 farClip.getY() / farClip.getW(),
267 farClip.getZ() / farClip.getW()
268 );
269
270 const Vector3 dir = farWorld - nearWorld;
271 if (dir.lengthSquared() < 1e-10f) {
272 return false;
273 }
274
275 outOrigin = nearWorld;
276 outDirection = dir.normalized();
277 return true;
278 }
279
280 Picker::Rect Picker::sanitizeRect(int x, int y, int width, int height) const
281 {
282 x = std::clamp(x, 0, std::max(0, _width - 1));
283 y = std::clamp(y, 0, std::max(0, _height - 1));
284 width = std::max(1, width);
285 height = std::max(1, height);
286 width = std::min(width, _width - x);
287 height = std::min(height, _height - y);
288 return Rect{ x, y, width, height };
289 }
290
291 bool Picker::isLayerAllowed(const std::vector<int>& objectLayers) const
292 {
293 if (_layers.empty()) {
294 return true;
295 }
296
297 return std::any_of(objectLayers.begin(), objectLayers.end(), [&](const int layer) {
298 return std::find(_layers.begin(), _layers.end(), layer) != _layers.end();
299 });
300 }
301}
Axis-Aligned Bounding Box defined by center and half-extents.
Definition boundingBox.h:21
const Vector3 & center() const
Definition boundingBox.h:27
const Vector3 & halfExtents() const
Definition boundingBox.h:35
Bounding sphere defined by center and radius for intersection and containment tests.
Central application orchestrator managing scenes, rendering, input, and resource loading.
Definition engine.h:38
Renderable instance of a Mesh with its own material, transform node, and optional GPU instancing.
void prepare(CameraComponent *camera, Scene *scene, const std::vector< int > &layers={})
Definition picker.cpp:36
MeshInstance * getSelectionSingle(int x, int y) const
Definition picker.cpp:157
Picker(Engine *app, int width, int height, bool depth=false)
Definition picker.cpp:24
int width() const
Definition picker.h:34
std::vector< MeshInstance * > getSelection(int x, int y, int width=1, int height=1) const
Definition picker.cpp:118
std::optional< Vector3 > getWorldPoint(int x, int y) const
Definition picker.cpp:163
void resize(int width, int height)
Definition picker.cpp:30
int height() const
Definition picker.h:35
Infinite ray defined by origin and direction for raycasting and picking.
Definition ray.h:14
static const std::vector< RenderComponent * > & instances()
Container for the scene graph, lighting environment, fog, skybox, and layer composition.
Definition scene.h:29
3D vector for positions, directions, and normals with multi-backend SIMD acceleration.
Definition vector3.h:29
float lengthSquared() const
Definition vector3.h:233
float length() const
Definition vector3.h:224