group2 0.1.0
CSE 125 Group 2
Loading...
Searching...
No Matches
Raycast.hpp
Go to the documentation of this file.
1
6
7#pragma once
8
18
19#include <algorithm>
20#include <cmath>
21#include <glm/common.hpp>
22#include <glm/geometric.hpp>
23
24namespace physics
25{
26
28inline constexpr float k_hitscanRange = 5000.0f;
29
31inline constexpr float k_parallelEpsilon = 1e-6f;
32
35{
36 bool hit{false};
38 glm::vec3 point{0.0f};
39 glm::vec3 normal{0.0f, 1.0f, 0.0f};
41 entt::entity entity{entt::null};
42};
43
45inline bool raycastAABB(glm::vec3 origin,
46 glm::vec3 direction,
47 const WorldAABB& box,
48 float maxDistance,
49 float& outDistance,
50 glm::vec3& outNormal)
51{
52 float tMin = 0.0f;
53 float tMax = maxDistance;
54 glm::vec3 hitNormal{0.0f};
55
56 for (int axis = 0; axis < 3; ++axis) {
57 if (std::abs(direction[axis]) < k_parallelEpsilon) {
58 if (origin[axis] < box.min[axis] || origin[axis] > box.max[axis]) {
59 return false;
60 }
61 continue;
62 }
63
64 const float invDir = 1.0f / direction[axis];
65 float t1 = (box.min[axis] - origin[axis]) * invDir;
66 float t2 = (box.max[axis] - origin[axis]) * invDir;
67 glm::vec3 axisNormal{0.0f};
68 axisNormal[axis] = (invDir >= 0.0f) ? -1.0f : 1.0f;
69
70 if (t1 > t2) {
71 std::swap(t1, t2);
72 axisNormal = -axisNormal;
73 }
74
75 if (t1 > tMin) {
76 tMin = t1;
77 hitNormal = axisNormal;
78 }
79
80 tMax = std::min(tMax, t2);
81 if (tMin > tMax) {
82 return false;
83 }
84 }
85
86 if (tMin < 0.0f || tMin > maxDistance) {
87 return false;
88 }
89
90 outDistance = tMin;
91 outNormal = hitNormal;
92 return true;
93}
94
96inline bool raycastCylinder(glm::vec3 origin,
97 glm::vec3 direction,
98 const WorldCylinder& cyl,
99 float maxDistance,
100 float& outDistance,
101 glm::vec3& outNormal)
102{
103 // XZ circle test
104 const float ox = origin.x - cyl.base.x;
105 const float oz = origin.z - cyl.base.z;
106 const float dx = direction.x;
107 const float dz = direction.z;
108
109 const float a = dx * dx + dz * dz;
110 const float b = 2.0f * (ox * dx + oz * dz);
111 const float c = ox * ox + oz * oz - cyl.radius * cyl.radius;
112
113 float tSide = maxDistance + 1.0f;
114 glm::vec3 sideNormal{0.0f};
115
116 if (a > k_parallelEpsilon) {
117 const float disc = b * b - 4.0f * a * c;
118 if (disc >= 0.0f) {
119 const float t = (-b - std::sqrt(disc)) / (2.0f * a);
120 if (t >= 0.0f && t < maxDistance) {
121 // Check Y bounds at hit point
122 const float hitY = origin.y + direction.y * t;
123 if (hitY >= cyl.base.y && hitY <= cyl.base.y + cyl.height) {
124 tSide = t;
125 sideNormal = glm::vec3(
126 origin.x + direction.x * t - cyl.base.x, 0.0f, origin.z + direction.z * t - cyl.base.z);
127 const float len = glm::length(sideNormal);
128 if (len > k_parallelEpsilon)
129 sideNormal /= len;
130 else
131 sideNormal = glm::vec3(1.0f, 0.0f, 0.0f);
132 }
133 }
134 }
135 }
136
137 // Cap tests (two discs)
138 float tCap = maxDistance + 1.0f;
139 glm::vec3 capNormal{0.0f};
140
141 if (std::abs(direction.y) > k_parallelEpsilon) {
142 // Bottom cap
143 float t = (cyl.base.y - origin.y) / direction.y;
144 if (t >= 0.0f && t < maxDistance) {
145 float hx = origin.x + direction.x * t - cyl.base.x;
146 float hz = origin.z + direction.z * t - cyl.base.z;
147 if (hx * hx + hz * hz <= cyl.radius * cyl.radius && t < tCap) {
148 tCap = t;
149 capNormal = glm::vec3(0.0f, -1.0f, 0.0f);
150 }
151 }
152 // Top cap
153 t = (cyl.base.y + cyl.height - origin.y) / direction.y;
154 if (t >= 0.0f && t < maxDistance) {
155 float hx = origin.x + direction.x * t - cyl.base.x;
156 float hz = origin.z + direction.z * t - cyl.base.z;
157 if (hx * hx + hz * hz <= cyl.radius * cyl.radius && t < tCap) {
158 tCap = t;
159 capNormal = glm::vec3(0.0f, 1.0f, 0.0f);
160 }
161 }
162 }
163
164 float best = maxDistance + 1.0f;
165 glm::vec3 bestN{0.0f};
166 if (tSide < best) {
167 best = tSide;
168 bestN = sideNormal;
169 }
170 if (tCap < best) {
171 best = tCap;
172 bestN = capNormal;
173 }
174
175 if (best > maxDistance)
176 return false;
177
178 outDistance = best;
179 outNormal = bestN;
180 return true;
181}
182
184inline bool raycastSphere(glm::vec3 origin,
185 glm::vec3 direction,
186 const WorldSphere& sph,
187 float maxDistance,
188 float& outDistance,
189 glm::vec3& outNormal)
190{
191 const glm::vec3 oc = origin - sph.center;
192 const float a = glm::dot(direction, direction);
193 if (a < k_parallelEpsilon)
194 return false;
195 const float b = 2.0f * glm::dot(oc, direction);
196 const float c = glm::dot(oc, oc) - sph.radius * sph.radius;
197
198 if (c <= 0.0f)
199 return false; // inside sphere
200
201 const float disc = b * b - 4.0f * a * c;
202 if (disc < 0.0f)
203 return false;
204
205 const float t = (-b - std::sqrt(disc)) / (2.0f * a);
206 if (t < 0.0f || t > maxDistance)
207 return false;
208
209 outDistance = t;
210 const glm::vec3 hitPos = origin + direction * t;
211 outNormal = glm::normalize(hitPos - sph.center);
212 return true;
213}
214
218inline bool raycastTriMeshIndexed(glm::vec3 origin,
219 glm::vec3 direction,
220 const WorldTriMesh& mesh,
221 float maxDistance,
222 float& outDistance,
223 glm::vec3& outNormal,
224 uint32_t& outTriIdx)
225{
226 // Quick reject against mesh AABB.
227 float dummyDist = maxDistance;
228 glm::vec3 dummyN{0.0f};
229 const WorldAABB meshBounds{mesh.boundsMin, mesh.boundsMax};
230 if (!raycastAABB(origin, direction, meshBounds, maxDistance, dummyDist, dummyN))
231 return false;
232
233 bool anyHit = false;
234 float bestDist = maxDistance;
235 glm::vec3 bestNormal{0.0f};
236 uint32_t bestTri = 0;
237
238 int stack[64];
239 int stackPtr = 0;
240 stack[0] = 0;
241
242 while (stackPtr >= 0) {
243 const int nodeIdx = stack[stackPtr--];
244 const auto& node = mesh.bvhNodes[static_cast<size_t>(nodeIdx)];
245
246 // Test ray against node AABB.
247 const WorldAABB nodeBox{node.boundsMin, node.boundsMax};
248 float nodeDist = bestDist;
249 glm::vec3 nodeN{0.0f};
250 if (!raycastAABB(origin, direction, nodeBox, bestDist, nodeDist, nodeN))
251 continue;
252
253 if (node.count > 0) {
254 // Leaf — Möller-Trumbore per triangle.
255 for (int i = node.leftFirst; i < node.leftFirst + node.count; ++i) {
256 const uint32_t ti = mesh.triIndices[static_cast<size_t>(i)];
257 const glm::vec3& v0 = mesh.vertices[mesh.indices[ti * 3 + 0]];
258 const glm::vec3& v1 = mesh.vertices[mesh.indices[ti * 3 + 1]];
259 const glm::vec3& v2 = mesh.vertices[mesh.indices[ti * 3 + 2]];
260
261 const glm::vec3 e1 = v1 - v0;
262 const glm::vec3 e2 = v2 - v0;
263 const glm::vec3 h = glm::cross(direction, e2);
264 const float a = glm::dot(e1, h);
265 if (std::abs(a) < 1e-8f)
266 continue;
267
268 const float f = 1.0f / a;
269 const glm::vec3 s = origin - v0;
270 const float u = f * glm::dot(s, h);
271 if (u < 0.0f || u > 1.0f)
272 continue;
273
274 const glm::vec3 q = glm::cross(s, e1);
275 const float v = f * glm::dot(direction, q);
276 if (v < 0.0f || u + v > 1.0f)
277 continue;
278
279 const float t = f * glm::dot(e2, q);
280 if (t > 0.0f && t < bestDist) {
281 bestDist = t;
282 bestNormal = glm::normalize(glm::cross(e1, e2));
283 if (glm::dot(bestNormal, direction) > 0.0f)
284 bestNormal = -bestNormal;
285 bestTri = ti;
286 anyHit = true;
287 }
288 }
289 } else {
290 stack[++stackPtr] = node.leftFirst;
291 stack[++stackPtr] = node.leftFirst + 1;
292 }
293 }
294
295 if (anyHit) {
296 outDistance = bestDist;
297 outNormal = bestNormal;
298 outTriIdx = bestTri;
299 }
300 return anyHit;
301}
302
304inline bool raycastTriMesh(glm::vec3 origin,
305 glm::vec3 direction,
306 const WorldTriMesh& mesh,
307 float maxDistance,
308 float& outDistance,
309 glm::vec3& outNormal)
310{
311 uint32_t ignored = 0;
312 return raycastTriMeshIndexed(origin, direction, mesh, maxDistance, outDistance, outNormal, ignored);
313}
314
323inline bool raycastBrush(glm::vec3 origin,
324 glm::vec3 direction,
325 const WorldBrush& brush,
326 float maxDistance,
327 float& outDistance,
328 glm::vec3& outNormal)
329{
330 float tEntry = 0.0f;
331 float tExit = maxDistance;
332 glm::vec3 entryNormal{0.0f, 1.0f, 0.0f};
333 bool startsOutside = false;
334
335 for (int i = 0; i < brush.planeCount; ++i) {
336 const Plane& plane = brush.planes[i];
337
338 // Signed distance from origin to plane (outward normal):
339 // > 0 → outside (free space)
340 // < 0 → inside (solid)
341 const float dist = glm::dot(plane.normal, origin) - plane.distance;
342 const float denom = glm::dot(plane.normal, direction);
343
344 if (dist > 0.0f)
345 startsOutside = true;
346
347 if (std::abs(denom) < k_parallelEpsilon) {
348 // Ray parallel to this plane: if origin is outside, the ray is
349 // entirely outside this plane's solid half-space → can't be in
350 // the brush at any t. If inside, this plane doesn't constrain
351 // the [tEntry, tExit] interval.
352 if (dist > 0.0f)
353 return false;
354 continue;
355 }
356
357 // t at which the ray crosses the plane.
358 const float t = -dist / denom;
359
360 if (denom < 0.0f) {
361 // Ray heading from outside (dist > 0) toward inside (dist < 0):
362 // entering this plane's solid half-space. Track the latest
363 // entry across all planes.
364 if (t > tEntry) {
365 tEntry = t;
366 entryNormal = plane.normal;
367 }
368 } else {
369 // Ray heading from inside toward outside: exiting. Track the
370 // earliest exit.
371 if (t < tExit)
372 tExit = t;
373 }
374 }
375
376 // Origin must be outside the brush; depenetration handles the inside case.
377 if (!startsOutside)
378 return false;
379 // Need a non-empty intersection window of [tEntry, tExit] within [0, maxDistance].
380 if (tEntry >= tExit || tEntry < 0.0f || tEntry >= maxDistance)
381 return false;
382
383 outDistance = tEntry;
384 outNormal = entryNormal;
385 return true;
386}
387
389inline HitscanHit raycastWorld(glm::vec3 origin, glm::vec3 direction, const WorldGeometry& world)
390{
391 HitscanHit bestHit;
392
393 for (const Plane& plane : world.planes) {
394 const float denom = glm::dot(plane.normal, direction);
395 if (std::abs(denom) < k_parallelEpsilon) {
396 continue;
397 }
398
399 const float distance = (plane.distance - glm::dot(plane.normal, origin)) / denom;
400 if (distance < 0.0f || distance >= bestHit.distance) {
401 continue;
402 }
403
404 bestHit.hit = true;
405 bestHit.distance = distance;
406 bestHit.point = origin + direction * distance;
407 bestHit.normal = plane.normal;
408 bestHit.surface = plane.surfaceType;
409 }
410
411 for (const WorldAABB& box : world.boxes) {
412 float distance = bestHit.distance;
413 glm::vec3 normal{0.0f};
414 if (!raycastAABB(origin, direction, box, bestHit.distance, distance, normal)) {
415 continue;
416 }
417
418 bestHit.hit = true;
419 bestHit.distance = distance;
420 bestHit.point = origin + direction * distance;
421 bestHit.normal = normal;
422 bestHit.surface = box.surfaceType;
423 }
424
425 for (const WorldBrush& brush : world.brushes) {
426 float distance = bestHit.distance;
427 glm::vec3 normal{0.0f};
428 if (!raycastBrush(origin, direction, brush, bestHit.distance, distance, normal))
429 continue;
430 bestHit.hit = true;
431 bestHit.distance = distance;
432 bestHit.point = origin + direction * distance;
433 bestHit.normal = normal;
434 bestHit.surface = brush.surfaceType;
435 }
436
437 for (const WorldCylinder& cyl : world.cylinders) {
438 float distance = bestHit.distance;
439 glm::vec3 normal{0.0f};
440 if (!raycastCylinder(origin, direction, cyl, bestHit.distance, distance, normal))
441 continue;
442 bestHit.hit = true;
443 bestHit.distance = distance;
444 bestHit.point = origin + direction * distance;
445 bestHit.normal = normal;
446 bestHit.surface = cyl.surfaceType;
447 }
448
449 for (const WorldSphere& sph : world.spheres) {
450 float distance = bestHit.distance;
451 glm::vec3 normal{0.0f};
452 if (!raycastSphere(origin, direction, sph, bestHit.distance, distance, normal))
453 continue;
454 bestHit.hit = true;
455 bestHit.distance = distance;
456 bestHit.point = origin + direction * distance;
457 bestHit.normal = normal;
458 bestHit.surface = sph.surfaceType;
459 }
460
461 auto testTriMesh = [&](const WorldTriMesh& tm) {
462 float distance = bestHit.distance;
463 glm::vec3 normal{0.0f};
464 uint32_t hitTriIdx = 0;
465 if (!raycastTriMeshIndexed(origin, direction, tm, bestHit.distance, distance, normal, hitTriIdx))
466 return;
467 bestHit.hit = true;
468 bestHit.distance = distance;
469 bestHit.point = origin + direction * distance;
470 bestHit.normal = normal;
471 // Per-triangle material if cooked, else mesh default.
472 bestHit.surface = (hitTriIdx < tm.triangleMaterials.size())
473 ? static_cast<SurfaceType>(tm.triangleMaterials[hitTriIdx])
474 : tm.defaultSurface;
475 };
476
477 const glm::vec3 triQueryEnd = origin + direction * bestHit.distance;
478 const WorldAABB triQuery{.min = glm::min(origin, triQueryEnd), .max = glm::max(origin, triQueryEnd)};
479 if (world.staticBroadphase != nullptr && !world.staticBroadphase->nodes.empty()) {
480 queryStaticWorldBroadphase(*world.staticBroadphase, triQuery, [&](uint32_t meshIndex) {
481 if (meshIndex < world.triMeshes.size())
482 testTriMesh(world.triMeshes[meshIndex]);
483 return true;
484 });
485 } else {
486 for (const WorldTriMesh& tm : world.triMeshes)
487 testTriMesh(tm);
488 }
489
490 return bestHit;
491}
492
494inline HitscanHit
495raycastPlayers(Registry& registry, entt::entity shooter, glm::vec3 origin, glm::vec3 direction, float maxDistance)
496{
497 HitscanHit bestHit;
498 bestHit.distance = maxDistance;
499
500 registry.view<Position, Player, CollisionShape>().each(
501 [&](const entt::entity entity, const Position& pos, const CollisionShape& shape) {
502 if (entity == shooter) {
503 return;
504 }
505
506 const WorldAABB bounds{
507 .min = pos.value - shape.halfExtents,
508 .max = pos.value + shape.halfExtents,
509 };
510
511 float distance = bestHit.distance;
512 glm::vec3 normal{0.0f};
513 if (!raycastAABB(origin, direction, bounds, bestHit.distance, distance, normal)) {
514 return;
515 }
516
517 bestHit.hit = true;
518 bestHit.distance = distance;
519 bestHit.point = origin + direction * distance;
520 bestHit.normal = normal;
521 bestHit.surface = SurfaceType::Flesh;
522 bestHit.entity = entity;
523 });
524
525 return bestHit;
526}
527
529inline HitscanHit resolveHitscan(Registry& registry, entt::entity shooter, glm::vec3 origin, glm::vec3 direction)
530{
531 HitscanHit bestHit = raycastWorld(origin, direction, activeWorld());
532
533 const HitscanHit playerHit = raycastPlayers(registry, shooter, origin, direction, bestHit.distance);
534 if (playerHit.hit && (!bestHit.hit || playerHit.distance < bestHit.distance)) {
535 bestHit = playerHit;
536 }
537
538 if (!bestHit.hit) {
539 bestHit.distance = k_hitscanRange;
540 bestHit.point = origin + direction * k_hitscanRange;
541 }
542
543 return bestHit;
544}
545
547
550{
551 bool hit{false};
553 glm::vec3 point{0.0f};
554 glm::vec3 normal{0.0f, 1.0f, 0.0f};
556 entt::entity entity{entt::null};
557};
558
566inline bool raycastCapsule(glm::vec3 origin,
567 glm::vec3 dir,
568 glm::vec3 A,
569 glm::vec3 B,
570 float r,
571 float maxDist,
572 float& outDist,
573 glm::vec3& outNormal)
574{
575 const glm::vec3 AB = B - A;
576 const float segLenSq = glm::dot(AB, AB);
577
578 // Degenerate capsule (zero-length segment) -> sphere test.
579 if (segLenSq < k_parallelEpsilon * k_parallelEpsilon) {
580 const WorldSphere sph{.center = A, .radius = r};
581 return raycastSphere(origin, dir, sph, maxDist, outDist, outNormal);
582 }
583
584 const float segLen = std::sqrt(segLenSq);
585 const glm::vec3 segDir = AB / segLen; // unit axis
586
587 // Project ray into cylinder-local coords where the cylinder axis is segDir.
588 // We test against the infinite cylinder first, then clamp.
589 const glm::vec3 oa = origin - A;
590
591 // Components perpendicular to cylinder axis:
592 // d_perp = dir - (dir . segDir) * segDir
593 // oa_perp = oa - (oa . segDir) * segDir
594 const float dirDotSeg = glm::dot(dir, segDir);
595 const float oaDotSeg = glm::dot(oa, segDir);
596
597 const glm::vec3 dPerp = dir - dirDotSeg * segDir;
598 const glm::vec3 oaPerp = oa - oaDotSeg * segDir;
599
600 const float a = glm::dot(dPerp, dPerp);
601 const float b = 2.0f * glm::dot(dPerp, oaPerp);
602 const float c = glm::dot(oaPerp, oaPerp) - r * r;
603
604 float bestT = maxDist + 1.0f;
605 glm::vec3 bestN{0.0f};
606
607 if (a > k_parallelEpsilon) {
608 const float disc = b * b - 4.0f * a * c;
609 if (disc >= 0.0f) {
610 const float sqrtDisc = std::sqrt(disc);
611 const float inv2a = 1.0f / (2.0f * a);
612
613 // Test both roots (entry and exit).
614 for (int side = 0; side < 2; ++side) {
615 const float t = (side == 0) ? (-b - sqrtDisc) * inv2a : (-b + sqrtDisc) * inv2a;
616 if (t < 0.0f || t > maxDist || t >= bestT)
617 continue;
618
619 // Where along the segment axis is the hit?
620 const float h = oaDotSeg + t * dirDotSeg;
621 if (h >= 0.0f && h <= segLen) {
622 // Hit the cylindrical shaft.
623 bestT = t;
624 const glm::vec3 hitPos = origin + dir * t;
625 const glm::vec3 closest = A + segDir * h;
626 bestN = glm::normalize(hitPos - closest);
627 }
628 }
629 }
630 }
631
632 // Hemisphere endcap tests (spheres at A and B).
633 auto testEndcap = [&](glm::vec3 center) {
634 float t = 0.0f;
635 glm::vec3 n{0.0f};
636 const WorldSphere sph{.center = center, .radius = r};
637 if (raycastSphere(origin, dir, sph, std::min(maxDist, bestT), t, n)) {
638 if (t < bestT) {
639 bestT = t;
640 bestN = n;
641 }
642 }
643 };
644 testEndcap(A);
645 testEndcap(B);
646
647 if (bestT > maxDist)
648 return false;
649
650 outDist = bestT;
651 outNormal = bestN;
652 return true;
653}
654
673 Registry& registry, entt::entity shooter, glm::vec3 origin, glm::vec3 direction, float maxDistance)
674{
675 HitboxHit bestHit;
676 bestHit.distance = maxDistance;
677
678 // PR-25: keep `Position, CollisionShape` as required components on
679 // the view to preserve the pre-PR-25 entity-set semantics (only
680 // animated PLAYER characters have all three) — but the broad-phase
681 // AABB is now computed from `hitboxes.capsules`, not from
682 // `Position + CollisionShape.halfExtents`. See the long comment
683 // below for why.
684 registry.view<Position, CollisionShape, HitboxInstance>().each(
685 [&](const entt::entity entity, const Position&, const CollisionShape&, const HitboxInstance& hitboxes) {
686 if (entity == shooter)
687 return;
688 if (hitboxes.capsules.empty())
689 return;
690
691 // PR-25: broad-phase AABB derived from the CAPSULES, not from
692 // `Position + CollisionShape.halfExtents`. Pre-PR-25 the
693 // broad-phase used the entity's live foot position + live
694 // halfExtents — fine when capsules tracked the live position,
695 // wrong after `rewindHitboxes` swapped capsules to a historical
696 // sample. At sprint speed (~700 u/s) and 200 ms RTT the
697 // historical capsules sit ~140 u away from the live AABB,
698 // outside the 32 u X/Z width of the standing player AABB →
699 // broad-phase rejected fast-moving rewound targets even though
700 // PR-24c had correctly placed historical capsules in the
701 // entity's `HitboxInstance`. Result: server reported miss
702 // even though the shot-debug visualizer showed the ray
703 // visibly crossing through the (rewound) capsule wireframe.
704 // Computing the AABB from the capsules makes the broad-phase
705 // always match the actual hit shape regardless of rewind state.
706 glm::vec3 boundsMin{std::numeric_limits<float>::max()};
707 glm::vec3 boundsMax{std::numeric_limits<float>::lowest()};
708 for (const WorldCapsule& cap : hitboxes.capsules) {
709 const glm::vec3 capMin = glm::min(cap.pointA, cap.pointB) - glm::vec3{cap.radius};
710 const glm::vec3 capMax = glm::max(cap.pointA, cap.pointB) + glm::vec3{cap.radius};
711 boundsMin = glm::min(boundsMin, capMin);
712 boundsMax = glm::max(boundsMax, capMax);
713 }
714 const WorldAABB bounds{.min = boundsMin, .max = boundsMax};
715 float aabbDist = bestHit.distance;
716 glm::vec3 aabbNormal{0.0f};
717 if (!raycastAABB(origin, direction, bounds, bestHit.distance, aabbDist, aabbNormal))
718 return;
719
720 // Narrow-phase: test each capsule.
721 for (const WorldCapsule& cap : hitboxes.capsules) {
722 float dist = bestHit.distance;
723 glm::vec3 normal{0.0f};
724 if (!raycastCapsule(
725 origin, direction, cap.pointA, cap.pointB, cap.radius, bestHit.distance, dist, normal))
726 continue;
727
728 bestHit.hit = true;
729 bestHit.distance = dist;
730 bestHit.point = origin + direction * dist;
731 bestHit.normal = normal;
732 bestHit.region = cap.region;
733 bestHit.entity = entity;
734 }
735 });
736
737 return bestHit;
738}
739
744inline HitboxHit resolveHitscanHitbox(Registry& registry, entt::entity shooter, glm::vec3 origin, glm::vec3 direction)
745{
746 // World geometry pass (returns HitscanHit but we need HitboxHit).
747 const HitscanHit worldHit = raycastWorld(origin, direction, activeWorld());
748
749 HitboxHit bestHit;
750 if (worldHit.hit) {
751 bestHit.hit = true;
752 bestHit.distance = worldHit.distance;
753 bestHit.point = worldHit.point;
754 bestHit.normal = worldHit.normal;
755 // entity stays entt::null (world geometry)
756 }
757
758 // Player hitbox pass.
759 const HitboxHit playerHit = raycastPlayerHitboxes(registry, shooter, origin, direction, bestHit.distance);
760 if (playerHit.hit && playerHit.distance < bestHit.distance) {
761 bestHit = playerHit;
762 }
763
764 if (!bestHit.hit) {
765 bestHit.distance = k_hitscanRange;
766 bestHit.point = origin + direction * k_hitscanRange;
767 }
768
769 return bestHit;
770}
771
772} // namespace physics
Collision shape component — AABB or vertical capsule.
Skeleton-driven hitbox types: body regions, capsule definitions, damage profiles, and per-entity runt...
BodyRegion
Body regions.
Definition Hitbox.hpp:23
@ UpperTorso
Definition Hitbox.hpp:26
Player component.
World-space position component for ECS entities.
Projectile component and weapon/surface type enumerations.
Shared ECS registry type alias for the game engine.
entt::registry Registry
Shared ECS registry type alias.
Definition Registry.hpp:11
SurfaceType
Surface material hit by a projectile / hitscan / contact.
Definition SurfaceType.hpp:28
@ Flesh
Definition SurfaceType.hpp:31
@ Concrete
Definition SurfaceType.hpp:30
Swept AABB and sphere collision queries against world geometry.
Triangle-mesh collision with BVH acceleration and seam welding.
Shared world geometry for collision / movement / raycast systems.
Pure physics math — no ECS types, no registry.
Definition BroadphaseTree.cpp:11
HitscanHit raycastPlayers(Registry &registry, entt::entity shooter, glm::vec3 origin, glm::vec3 direction, float maxDistance)
Raycast against all player hitboxes (axis-aligned capsule approximation).
Definition Raycast.hpp:495
constexpr float k_hitscanRange
Maximum hitscan distance in world units.
Definition Raycast.hpp:28
bool raycastTriMeshIndexed(glm::vec3 origin, glm::vec3 direction, const WorldTriMesh &mesh, float maxDistance, float &outDistance, glm::vec3 &outNormal, uint32_t &outTriIdx)
Raycast against a triangle mesh using BVH-accelerated Möller-Trumbore.
Definition Raycast.hpp:218
bool raycastCapsule(glm::vec3 origin, glm::vec3 dir, glm::vec3 A, glm::vec3 B, float r, float maxDist, float &outDist, glm::vec3 &outNormal)
Ray vs capsule intersection.
Definition Raycast.hpp:566
bool raycastSphere(glm::vec3 origin, glm::vec3 direction, const WorldSphere &sph, float maxDistance, float &outDistance, glm::vec3 &outNormal)
Ray vs sphere intersection.
Definition Raycast.hpp:184
const WorldGeometry & activeWorld()
Return the world geometry most recently set via setActiveWorld(), or fall back to testWorld() if none...
Definition WorldData.hpp:236
bool raycastAABB(glm::vec3 origin, glm::vec3 direction, const WorldAABB &box, float maxDistance, float &outDistance, glm::vec3 &outNormal)
Ray vs axis-aligned box intersection (slab method).
Definition Raycast.hpp:45
HitboxHit resolveHitscanHitbox(Registry &registry, entt::entity shooter, glm::vec3 origin, glm::vec3 direction)
Full hitscan with skeleton-driven hitboxes.
Definition Raycast.hpp:744
bool raycastCylinder(glm::vec3 origin, glm::vec3 direction, const WorldCylinder &cyl, float maxDistance, float &outDistance, glm::vec3 &outNormal)
Ray vs vertical cylinder intersection.
Definition Raycast.hpp:96
HitscanHit resolveHitscan(Registry &registry, entt::entity shooter, glm::vec3 origin, glm::vec3 direction)
Full hitscan resolution: world geometry first, then players (closest wins).
Definition Raycast.hpp:529
HitscanHit raycastWorld(glm::vec3 origin, glm::vec3 direction, const WorldGeometry &world)
Raycast against all static world geometry (planes + boxes + cylinders + spheres).
Definition Raycast.hpp:389
constexpr float k_parallelEpsilon
Epsilon for parallel-ray checks.
Definition Raycast.hpp:31
bool raycastBrush(glm::vec3 origin, glm::vec3 direction, const WorldBrush &brush, float maxDistance, float &outDistance, glm::vec3 &outNormal)
Ray vs convex brush intersection (generalised slab method).
Definition Raycast.hpp:323
HitboxHit raycastPlayerHitboxes(Registry &registry, entt::entity shooter, glm::vec3 origin, glm::vec3 direction, float maxDistance)
Raycast against all player hitbox capsules (skeleton-driven).
Definition Raycast.hpp:672
void queryStaticWorldBroadphase(const StaticWorldBroadphase &broadphase, const WorldAABB &query, const std::function< bool(uint32_t meshIndex)> &visit)
Visit triangle-mesh indices whose mesh bounds overlap query.
Definition SweptCollision.cpp:217
bool raycastTriMesh(glm::vec3 origin, glm::vec3 direction, const WorldTriMesh &mesh, float maxDistance, float &outDistance, glm::vec3 &outNormal)
Convenience wrapper for callers that don't care which triangle was hit.
Definition Raycast.hpp:304
Collision shape attached to an entity.
Definition CollisionShape.hpp:34
glm::vec3 halfExtents
AABB half-dimensions.
Definition CollisionShape.hpp:41
ECS components.
Definition Hitbox.hpp:108
std::vector< WorldCapsule > capsules
~12 capsules per character.
Definition Hitbox.hpp:109
Result of a hitscan raycast.
Definition Raycast.hpp:35
ECS tag component: marks an entity as a player (vs. projectile, spawner, etc.).
Definition Player.hpp:8
World-space position of an entity, in game units.
Definition Position.hpp:10
glm::vec3 value
XYZ position (Y-up, Quake units).
Definition Position.hpp:11
Runtime capsule (world-space, per-entity, per-frame).
Definition Hitbox.hpp:95
glm::vec3 pointA
One endpoint of the capsule centerline.
Definition Hitbox.hpp:96
float radius
Capsule radius (world units).
Definition Hitbox.hpp:98
glm::vec3 pointB
Other endpoint of the capsule centerline.
Definition Hitbox.hpp:97
BodyRegion region
For damage multiplier lookup.
Definition Hitbox.hpp:99
Skeleton-driven hitbox raycast (capsule-based).
Definition Raycast.hpp:550
BodyRegion region
Definition Raycast.hpp:555
entt::entity entity
Definition Raycast.hpp:556
glm::vec3 point
Definition Raycast.hpp:553
float distance
Definition Raycast.hpp:552
glm::vec3 normal
Definition Raycast.hpp:554
bool hit
Definition Raycast.hpp:551
Result of a hitscan raycast.
Definition Raycast.hpp:35
SurfaceType surface
Definition Raycast.hpp:40
bool hit
Definition Raycast.hpp:36
float distance
Definition Raycast.hpp:37
glm::vec3 normal
Definition Raycast.hpp:39
glm::vec3 point
Definition Raycast.hpp:38
entt::entity entity
Definition Raycast.hpp:41
An infinite plane dividing free space from solid geometry.
Definition SweptCollision.hpp:31
SurfaceType surfaceType
Material tag for impact VFX / SFX (Phase 3).
Definition SweptCollision.hpp:34
glm::vec3 normal
Unit vector pointing into free (non-solid) space.
Definition SweptCollision.hpp:32
float distance
Signed offset: dot(normal, p) == distance for points on the plane.
Definition SweptCollision.hpp:33
std::vector< BVHNode > nodes
Definition SweptCollision.hpp:150
An axis-aligned box in world space, used as static collision geometry.
Definition SweptCollision.hpp:39
SurfaceType surfaceType
Material tag (Phase 3).
Definition SweptCollision.hpp:42
glm::vec3 max
Maximum corner (highest x, y, z).
Definition SweptCollision.hpp:41
glm::vec3 min
Minimum corner (lowest x, y, z).
Definition SweptCollision.hpp:40
A convex volume defined by bounding planes (for ramps, angled walls, etc.).
Definition SweptCollision.hpp:58
Plane planes[k_maxPlanes]
Definition SweptCollision.hpp:60
SurfaceType surfaceType
Material tag (Phase 3).
Definition SweptCollision.hpp:62
int planeCount
Definition SweptCollision.hpp:61
A vertical (Y-axis) cylinder in world space.
Definition SweptCollision.hpp:70
float radius
Horizontal radius.
Definition SweptCollision.hpp:72
float height
Extent along +Y from base.
Definition SweptCollision.hpp:73
glm::vec3 base
Centre of the bottom cap.
Definition SweptCollision.hpp:71
SurfaceType surfaceType
Material tag (Phase 3).
Definition SweptCollision.hpp:74
All world collision geometry for one tick.
Definition SweptCollision.hpp:225
std::span< const WorldCylinder > cylinders
Definition SweptCollision.hpp:229
std::span< const WorldTriMesh > triMeshes
Definition SweptCollision.hpp:231
const StaticWorldBroadphase * staticBroadphase
Definition SweptCollision.hpp:232
std::span< const Plane > planes
Definition SweptCollision.hpp:226
std::span< const WorldAABB > boxes
Definition SweptCollision.hpp:227
std::span< const WorldBrush > brushes
Definition SweptCollision.hpp:228
std::span< const WorldSphere > spheres
Definition SweptCollision.hpp:230
A sphere in world space.
Definition SweptCollision.hpp:79
SurfaceType surfaceType
Material tag (Phase 3).
Definition SweptCollision.hpp:82
glm::vec3 center
Centre point.
Definition SweptCollision.hpp:80
float radius
Radius.
Definition SweptCollision.hpp:81
A triangle mesh with BVH acceleration for collision queries.
Definition SweptCollision.hpp:108
std::vector< uint32_t > indices
Triangle indices (3 per triangle).
Definition SweptCollision.hpp:110
std::vector< BVHNode > bvhNodes
Flat BVH node array.
Definition SweptCollision.hpp:111
std::vector< glm::vec3 > vertices
All vertex positions (world space, scaled).
Definition SweptCollision.hpp:109
glm::vec3 boundsMin
Whole-mesh AABB min.
Definition SweptCollision.hpp:113
glm::vec3 boundsMax
Whole-mesh AABB max.
Definition SweptCollision.hpp:114
std::vector< uint32_t > triIndices
Permutation: BVH leaf ranges → triangle indices.
Definition SweptCollision.hpp:112