VisuTwin Canvas
C++ 3D Engine — Metal Backend
Loading...
Searching...
No Matches
rigidBodyComponentSystem.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2025-2026 Arnis Lektauers
4
5#include <algorithm>
6#include <cmath>
7#include <limits>
8
11
12namespace visutwin::canvas
13{
14 namespace
15 {
16 constexpr float EPS = 1e-6f;
17
18 bool intersectSegmentSphere(
19 const Vector3& start, const Vector3& end, const Vector3& center, const float radius,
20 float& outT, Vector3& outPoint, Vector3& outNormal)
21 {
22 const Vector3 d = end - start;
23 const float a = d.dot(d);
24 if (a <= 1e-10f) {
25 return false;
26 }
27
28 const Vector3 m = start - center;
29 const float b = m.dot(d);
30 const float c = m.dot(m) - radius * radius;
31 if (c > 0.0f && b > 0.0f) {
32 return false;
33 }
34
35 const float discr = b * b - a * c;
36 if (discr < 0.0f) {
37 return false;
38 }
39
40 const float sqrtDiscr = std::sqrt(discr);
41 float t = (-b - sqrtDiscr) / a;
42 if (t < 0.0f || t > 1.0f) {
43 t = (-b + sqrtDiscr) / a;
44 if (t < 0.0f || t > 1.0f) {
45 return false;
46 }
47 }
48
49 outT = t;
50 outPoint = start + d * t;
51 outNormal = (outPoint - center).normalized();
52 if (outNormal.lengthSquared() <= 1e-8f) {
53 outNormal = Vector3(0.0f, 1.0f, 0.0f);
54 }
55 return true;
56 }
57
58 bool intersectSegmentAabb(
59 const Vector3& start, const Vector3& end, const Vector3& halfExtents,
60 float& outT, Vector3& outPoint, Vector3& outNormal)
61 {
62 const Vector3 d = end - start;
63 float tMin = 0.0f;
64 float tMax = 1.0f;
65 Vector3 hitNormal(0.0f, 0.0f, 0.0f);
66
67 const float minB[3] = {-halfExtents.getX(), -halfExtents.getY(), -halfExtents.getZ()};
68 const float maxB[3] = { halfExtents.getX(), halfExtents.getY(), halfExtents.getZ()};
69 const float s[3] = {start.getX(), start.getY(), start.getZ()};
70 const float v[3] = {d.getX(), d.getY(), d.getZ()};
71
72 for (int axis = 0; axis < 3; ++axis) {
73 if (std::abs(v[axis]) < EPS) {
74 if (s[axis] < minB[axis] || s[axis] > maxB[axis]) {
75 return false;
76 }
77 continue;
78 }
79
80 float t1 = (minB[axis] - s[axis]) / v[axis];
81 float t2 = (maxB[axis] - s[axis]) / v[axis];
82 float enter = std::min(t1, t2);
83 float exit = std::max(t1, t2);
84
85 if (enter > tMin) {
86 tMin = enter;
87 if (axis == 0) hitNormal = Vector3((t1 > t2) ? 1.0f : -1.0f, 0.0f, 0.0f);
88 if (axis == 1) hitNormal = Vector3(0.0f, (t1 > t2) ? 1.0f : -1.0f, 0.0f);
89 if (axis == 2) hitNormal = Vector3(0.0f, 0.0f, (t1 > t2) ? 1.0f : -1.0f);
90 }
91 tMax = std::min(tMax, exit);
92 if (tMin > tMax) {
93 return false;
94 }
95 }
96
97 if (tMin < 0.0f || tMin > 1.0f) {
98 return false;
99 }
100
101 outT = tMin;
102 outPoint = start + d * tMin;
103 outNormal = hitNormal.lengthSquared() > EPS ? hitNormal : Vector3(0.0f, 1.0f, 0.0f);
104 return true;
105 }
106
107 bool intersectSegmentCapsuleY(
108 const Vector3& start, const Vector3& end, const float radius, const float height,
109 float& outT, Vector3& outPoint, Vector3& outNormal)
110 {
111 const Vector3 d = end - start;
112 const float halfLine = std::max(0.0f, height * 0.5f - radius);
113 bool found = false;
114 float bestT = std::numeric_limits<float>::max();
115 Vector3 bestPoint;
116 Vector3 bestNormal;
117
118 // Cylinder body (around Y axis).
119 const float a = d.getX() * d.getX() + d.getZ() * d.getZ();
120 const float b = 2.0f * (start.getX() * d.getX() + start.getZ() * d.getZ());
121 const float c = start.getX() * start.getX() + start.getZ() * start.getZ() - radius * radius;
122 if (a > EPS) {
123 const float disc = b * b - 4.0f * a * c;
124 if (disc >= 0.0f) {
125 const float sqrtDisc = std::sqrt(disc);
126 const float tCand[2] = {
127 (-b - sqrtDisc) / (2.0f * a),
128 (-b + sqrtDisc) / (2.0f * a)
129 };
130 for (const float t : tCand) {
131 if (t < 0.0f || t > 1.0f) {
132 continue;
133 }
134 const float y = start.getY() + d.getY() * t;
135 if (y < -halfLine || y > halfLine) {
136 continue;
137 }
138 if (t < bestT) {
139 const Vector3 p = start + d * t;
140 const Vector3 n = Vector3(p.getX(), 0.0f, p.getZ()).normalized();
141 bestT = t;
142 bestPoint = p;
143 bestNormal = n.lengthSquared() > EPS ? n : Vector3(1.0f, 0.0f, 0.0f);
144 found = true;
145 }
146 }
147 }
148 }
149
150 // Hemispherical caps.
151 const Vector3 capTop(0.0f, halfLine, 0.0f);
152 const Vector3 capBottom(0.0f, -halfLine, 0.0f);
153 float tSphere = 0.0f;
154 Vector3 pSphere;
155 Vector3 nSphere;
156 if (intersectSegmentSphere(start, end, capTop, radius, tSphere, pSphere, nSphere) && tSphere < bestT) {
157 bestT = tSphere;
158 bestPoint = pSphere;
159 bestNormal = nSphere;
160 found = true;
161 }
162 if (intersectSegmentSphere(start, end, capBottom, radius, tSphere, pSphere, nSphere) && tSphere < bestT) {
163 bestT = tSphere;
164 bestPoint = pSphere;
165 bestNormal = nSphere;
166 found = true;
167 }
168
169 if (!found) {
170 return false;
171 }
172
173 outT = bestT;
174 outPoint = bestPoint;
175 outNormal = bestNormal;
176 return true;
177 }
178
179 // DEVIATION: until full Ammo/Bullet integration lands, raycast uses analytic primitive
180 // intersections against current CollisionComponent shapes transformed by entity world matrix.
181 bool intersectCollisionShape(
182 const CollisionComponent* collision, const Vector3& start, const Vector3& end,
183 float& outT, Vector3& outPoint, Vector3& outNormal)
184 {
185 if (!collision || !collision->entity()) {
186 return false;
187 }
188
189 const Matrix4 world = collision->entity()->worldTransform();
190 const Matrix4 invWorld = world.inverse();
191 const Vector3 localStart = invWorld.transformPoint(start);
192 const Vector3 localEnd = invWorld.transformPoint(end);
193
194 float localT = 0.0f;
195 Vector3 localPoint;
196 Vector3 localNormal;
197
198 const std::string type = collision->type();
199 bool hit = false;
200 if (type == "box") {
201 hit = intersectSegmentAabb(localStart, localEnd, collision->halfExtents(), localT, localPoint, localNormal);
202 } else if (type == "sphere") {
203 hit = intersectSegmentSphere(localStart, localEnd, Vector3(0.0f, 0.0f, 0.0f), collision->radius(),
204 localT, localPoint, localNormal);
205 } else if (type == "capsule") {
206 hit = intersectSegmentCapsuleY(localStart, localEnd, collision->radius(), collision->height(),
207 localT, localPoint, localNormal);
208 } else {
209 // Fallback for unsupported shapes: conservative world-bounds sphere.
210 float sphereT = 0.0f;
211 Vector3 spherePoint;
212 Vector3 sphereNormal;
213 const BoundingSphere sphere = collision->worldBounds();
214 hit = intersectSegmentSphere(start, end, sphere.center(), sphere.radius(), sphereT, spherePoint, sphereNormal);
215 if (hit) {
216 outT = sphereT;
217 outPoint = spherePoint;
218 outNormal = sphereNormal;
219 return true;
220 }
221 }
222
223 if (!hit) {
224 return false;
225 }
226
227 const Vector3 worldPoint = world.transformPoint(localPoint);
228 Vector3 worldNormal = localNormal.transformNormal(world).normalized();
229 if (worldNormal.lengthSquared() <= EPS) {
230 worldNormal = Vector3(0.0f, 1.0f, 0.0f);
231 }
232
233 outT = std::clamp(localT, 0.0f, 1.0f);
234 outPoint = worldPoint;
235 outNormal = worldNormal;
236 return true;
237 }
238 }
239
240 std::optional<RaycastResult> RigidBodyComponentSystem::raycastFirst(const Vector3& start, const Vector3& end) const
241 {
242 const auto all = raycastAll(start, end);
243 if (all.empty()) {
244 return std::nullopt;
245 }
246 return all.front();
247 }
248
249 std::vector<RaycastResult> RigidBodyComponentSystem::raycastAll(const Vector3& start, const Vector3& end) const
250 {
251 std::vector<RaycastResult> results;
252 results.reserve(RigidBodyComponent::instances().size());
253
254 for (auto* rigidbody : RigidBodyComponent::instances()) {
255 if (!rigidbody || !rigidbody->enabled() || !rigidbody->entity()) {
256 continue;
257 }
258
259 auto* collision = rigidbody->collision();
260 if (!collision || !collision->enabled()) {
261 continue;
262 }
263
264 float t = 0.0f;
265 Vector3 point;
266 Vector3 normal;
267 if (!intersectCollisionShape(collision, start, end, t, point, normal)) {
268 continue;
269 }
270
271 RaycastResult result;
272 result.entity = rigidbody->entity();
273 result.collision = collision;
274 result.rigidbody = rigidbody;
275 result.point = point;
276 result.normal = normal;
277 result.hitFraction = t;
278 results.push_back(result);
279 }
280
281 std::sort(results.begin(), results.end(), [](const RaycastResult& a, const RaycastResult& b) {
282 if (std::abs(a.hitFraction - b.hitFraction) > 1e-6f) {
283 return a.hitFraction < b.hitFraction;
284 }
285 return a.entity < b.entity;
286 });
287
288 return results;
289 }
290}
Bounding sphere defined by center and radius for intersection and containment tests.
static const std::vector< RigidBodyComponent * > & instances()
std::optional< RaycastResult > raycastFirst(const Vector3 &start, const Vector3 &end) const
std::vector< RaycastResult > raycastAll(const Vector3 &start, const Vector3 &end) const
4x4 column-major transformation matrix with SIMD acceleration.
Definition matrix4.h:31
Matrix4 inverse() const
3D vector for positions, directions, and normals with multi-backend SIMD acceleration.
Definition vector3.h:29
Vector3 normalized() const
Vector3 transformNormal(const Matrix4 &mat) const
float dot(const Vector3 &other) const
float lengthSquared() const
Definition vector3.h:233