载入中...
搜索中...
未找到
TrajectoryCollision.cpp
浏览该文件的文档.
2
4#include "physics/Body3D.h"
5#include "physics/Shape3D.h"
6#include "physics/World3D.h"
7
8#include <cmath>
9#include <utility>
10
12namespace {
13
14AttachmentPoint addOffset(AttachmentPoint base, float dx, float dy, float dz) {
15 return {base.x + dx, base.y + dy, base.z + dz};
16}
17
18bool finiteSample(const TrajectorySample& sample) {
19 return sample.valid && std::isfinite(sample.start.x) && std::isfinite(sample.start.y) &&
20 std::isfinite(sample.start.z) && std::isfinite(sample.end.x) && std::isfinite(sample.end.y) &&
21 std::isfinite(sample.end.z);
22}
23
24class ScopedQueryFilter {
25public:
26 ScopedQueryFilter(World3D& world, QueryFilter3D filter)
27 : world_(world), category_(world.getQueryCategoryBits()), mask_(world.getQueryMaskBits()),
28 body_(world.getQueryIgnoredBodyId()), shape_(world.getQueryIgnoredShapeId()) {
29 world_.setQueryFilter(static_cast<int>(filter.categoryBits), static_cast<int>(filter.maskBits));
30 world_.setQueryIgnoredBodyId(filter.ignoredBodyId);
31 world_.setQueryIgnoredShapeId(filter.ignoredShapeId);
32 }
33 ~ScopedQueryFilter() noexcept {
34 world_.setQueryFilter(category_, mask_);
35 world_.setQueryIgnoredBodyId(body_);
36 world_.setQueryIgnoredShapeId(shape_);
37 }
38
39private:
40 World3D& world_;
41 int category_;
42 int mask_;
43 int body_;
44 int shape_;
45};
46
47} // namespace
48
50 : world_(&world), worldLifetime_(world.lifetimeToken()), worldHandle_(world.runtimeHandle()) {}
51
53
54World3D* TrajectoryCollisionRuntime::liveWorld() const noexcept {
55 auto lifetime = worldLifetime_.lock();
56 if (!lifetime || !world_ || !world_->isValid() || world_->runtimeHandle() != worldHandle_) return nullptr;
57 return world_;
58}
59
61 auto valid = definition.validate();
62 if (!valid) return Result<void>::failure(valid.status());
63 colliders_[definition.colliderId] = std::move(definition);
64 return Result<void>::success();
65}
66
68 if (colliderId.empty())
70 Diagnostic::error(DiagnosticCode::InvalidArgument, "bone collider id is empty", "colliderId"));
71 const std::string id(colliderId);
72 const auto found = colliders_.find(id);
73 if (found == colliders_.end())
75 Diagnostic::error(DiagnosticCode::NotFound, "bone collider catalog entry missing", id));
76
77 for (auto it = armed_.begin(); it != armed_.end();) {
78 if (it->second.colliderId == id)
79 it = armed_.erase(it);
80 else
81 ++it;
82 }
83 for (auto it = hitMemory_.begin(); it != hitMemory_.end();) {
84 if (std::get<1>(it->first) == id)
85 it = hitMemory_.erase(it);
86 else
87 ++it;
88 }
89 colliders_.erase(found);
90 return Result<void>::success();
91}
92
100
102 if (!subject.isValid())
104 Diagnostic::error(DiagnosticCode::InvalidArgument, "trajectory subject is nil", "subject"));
105 const std::string key = subject.format();
106 poses_.erase(key);
107 for (auto it = armed_.begin(); it != armed_.end();) {
108 if (it->second.subject.format() == key)
109 it = armed_.erase(it);
110 else
111 ++it;
112 }
113 for (auto it = hitMemory_.begin(); it != hitMemory_.end();) {
114 if (std::get<0>(it->first) == key)
115 it = hitMemory_.erase(it);
116 else
117 ++it;
118 }
119 return Result<void>::success();
120}
121
122Result<TrajectorySample> TrajectoryCollisionRuntime::sampleCollider(
123 SubjectRef subject, const BoneColliderDefinition& def) const {
124 const auto poseIt = poses_.find(subject.format());
125 if (poseIt == poses_.end() || poseIt->second == nullptr)
127 DiagnosticCode::PreconditionViolation, "trajectory pose source is unbound", subject.format()));
128
129 IAttachmentPointSource& source = *poseIt->second;
130 TrajectorySample sample;
132 auto point = source.sampleAttachmentPoint(def.boneName, def.localOffset);
133 if (!point) return Result<TrajectorySample>::failure(point.status());
134 sample.start = point.value();
135 sample.end = point.value();
136 sample.valid = true;
138 }
139
140 if (!def.endBoneName.empty()) {
141 auto start = source.sampleAttachmentPoint(def.boneName, def.localOffset);
142 if (!start) return Result<TrajectorySample>::failure(start.status());
143 auto end = source.sampleAttachmentPoint(def.endBoneName, def.endLocalOffset);
144 if (!end) return Result<TrajectorySample>::failure(end.status());
145 sample.start = start.value();
146 sample.end = end.value();
147 sample.valid = true;
149 }
150
151 const AttachmentPoint aLocal =
152 addOffset(def.localOffset, 0.f, -def.halfHeight, 0.f);
153 const AttachmentPoint bLocal = addOffset(def.localOffset, 0.f, def.halfHeight, 0.f);
154 auto start = source.sampleAttachmentPoint(def.boneName, aLocal);
155 if (!start) return Result<TrajectorySample>::failure(start.status());
156 auto end = source.sampleAttachmentPoint(def.boneName, bLocal);
157 if (!end) return Result<TrajectorySample>::failure(end.status());
158 sample.start = start.value();
159 sample.end = end.value();
160 sample.valid = true;
162}
163
165 if (!subject.isValid())
167 Diagnostic::error(DiagnosticCode::InvalidArgument, "trajectory subject is nil", "subject"));
168 if (colliderId.empty())
170 Diagnostic::error(DiagnosticCode::InvalidArgument, "bone collider id is empty", "colliderId"));
171 const std::string id(colliderId);
172 const auto catalog = colliders_.find(id);
173 if (catalog == colliders_.end())
175 Diagnostic::error(DiagnosticCode::NotFound, "bone collider catalog entry missing", id));
176 if (!liveWorld())
178 Diagnostic::error(DiagnosticCode::StaleHandle, "trajectory world is no longer live", "world"));
179
180 auto sample = sampleCollider(subject, catalog->second);
181 if (!sample) return Result<void>::failure(sample.status());
182
183 ArmedKey key{subject.format(), id};
184 ArmedCollider armed;
185 armed.subject = subject;
186 armed.colliderId = id;
187 armed.previous = sample.value();
188 armed.current = sample.value();
189 armed.hasSample = true;
190 armed_[key] = std::move(armed);
191 return Result<void>::success();
192}
193
195 if (!subject.isValid())
197 Diagnostic::error(DiagnosticCode::InvalidArgument, "trajectory subject is nil", "subject"));
198 if (colliderId.empty())
200 Diagnostic::error(DiagnosticCode::InvalidArgument, "bone collider id is empty", "colliderId"));
201 const std::string id(colliderId);
202 const ArmedKey key{subject.format(), id};
203 const auto found = armed_.find(key);
204 if (found == armed_.end())
206 Diagnostic::error(DiagnosticCode::NotFound, "armed bone collider is missing", id));
207 armed_.erase(found);
208 for (auto it = hitMemory_.begin(); it != hitMemory_.end();) {
209 if (std::get<0>(it->first) == subject.format() && std::get<1>(it->first) == id)
210 it = hitMemory_.erase(it);
211 else
212 ++it;
213 }
214 return Result<void>::success();
215}
216
217Result<void> TrajectoryCollisionRuntime::sweepArmed(World3D& world, ArmedCollider& armed,
218 const BoneColliderDefinition& def,
219 std::vector<TrajectoryHit>& outHits) {
220 if (!finiteSample(armed.previous) || !finiteSample(armed.current))
222 "trajectory sample is non-finite", "sample"));
223
224 const float dx = armed.current.start.x - armed.previous.start.x;
225 const float dy = armed.current.start.y - armed.previous.start.y;
226 const float dz = armed.current.start.z - armed.previous.start.z;
227 // Stationary frame: still overlap-test with a zero-length cast so resting contacts register once.
230 filter.maskBits = def.maskBits;
231 filter.ignoredBodyId = def.ignoredBodyId;
232
233 ScopedQueryFilter scoped(world, filter);
234 int hitCount = 0;
236 hitCount = world.castSphereAll(armed.previous.start.x, armed.previous.start.y, armed.previous.start.z,
237 def.radius, dx, dy, dz, def.maxHits);
238 } else {
239 // Capsule endpoints move by the same translation as the start sample. Dual-socket blades
240 // whose endpoints diverge within one frame are approximated by the start delta (UE5
241 // single-sweep weapon traces use the same previous→current translation).
242 hitCount =
243 world.castCapsuleAll(armed.previous.start.x, armed.previous.start.y, armed.previous.start.z,
244 armed.previous.end.x, armed.previous.end.y, armed.previous.end.z, def.radius, dx,
245 dy, dz, def.maxHits);
246 }
247
248 for (int i = 0; i < hitCount; ++i) {
249 const int bodyId = world.getShapeCastResultBodyId(i);
250 const int shapeId = world.getShapeCastResultShapeId(i);
251 HitMemoryKey memory{armed.subject.format(), armed.colliderId, bodyId, shapeId};
252 if (hitMemory_.contains(memory)) continue;
253 hitMemory_[memory] = true;
254
255 TrajectoryHit hit;
256 hit.subject = armed.subject;
257 hit.colliderId = armed.colliderId;
258 hit.world = worldHandle_;
259 hit.bodyId = bodyId;
260 hit.shapeId = shapeId;
261 hit.shapeTag = world.getShapeCastResultShapeTag(i);
262 hit.materialId = world.getShapeCastResultMaterialId(i);
263 hit.x = world.getShapeCastResultX(i);
264 hit.y = world.getShapeCastResultY(i);
265 hit.z = world.getShapeCastResultZ(i);
266 hit.normalX = world.getShapeCastResultNormalX(i);
267 hit.normalY = world.getShapeCastResultNormalY(i);
268 hit.normalZ = world.getShapeCastResultNormalZ(i);
269 hit.fraction = world.getShapeCastResultFraction(i);
270 if (Body3D* body = world.findBodyById(bodyId)) hit.body = body->runtimeHandle();
271 if (Shape3D* shape = world.findShapeById(shapeId)) hit.shape = shape->runtimeHandle();
272 outHits.push_back(std::move(hit));
273 }
274 return Result<void>::success();
275}
276
278 if (tick.value() < lastTick_.value())
280 DiagnosticCode::InvalidArgument, "trajectory tick must be non-decreasing", "tick"));
281 World3D* world = liveWorld();
282 if (!world)
284 Diagnostic::error(DiagnosticCode::StaleHandle, "trajectory world is no longer live", "world"));
285
286 TrajectoryFrame frame;
287 frame.tick = tick;
288
289 for (auto& [key, armed] : armed_) {
290 (void)key;
291 const auto catalog = colliders_.find(armed.colliderId);
292 if (catalog == colliders_.end())
294 DiagnosticCode::NotFound, "armed bone collider catalog entry missing", armed.colliderId));
295
296 auto sample = sampleCollider(armed.subject, catalog->second);
297 if (!sample) return Result<TrajectoryFrame>::failure(sample.status());
298 armed.previous = armed.current;
299 armed.current = sample.value();
300 armed.hasSample = true;
301
302 auto swept = sweepArmed(*world, armed, catalog->second, frame.hits);
303 if (!swept) return Result<TrajectoryFrame>::failure(swept.status());
304 }
305
306 lastTick_ = tick;
307 return Result<TrajectoryFrame>::success(std::move(frame));
308}
309
311 std::string_view colliderId) const {
312 if (!subject.isValid())
314 Diagnostic::error(DiagnosticCode::InvalidArgument, "trajectory subject is nil", "subject"));
315 if (colliderId.empty())
317 Diagnostic::error(DiagnosticCode::InvalidArgument, "bone collider id is empty", "colliderId"));
318 const ArmedKey key{subject.format(), std::string(colliderId)};
319 const auto found = armed_.find(key);
320 if (found == armed_.end() || !found->second.hasSample)
322 Diagnostic::error(DiagnosticCode::NotFound, "armed bone collider sample missing",
323 std::string(colliderId)));
324 return Result<TrajectorySample>::success(found->second.current);
325}
326
327} // namespace eve::physics::trajectory
Duration start
int subject
Definition AnimSmr.cpp:163
std::uint32_t key
ShaderImageInput shape
vk::UniqueDeviceMemory memory
bool valid
World3D * world
std::weak_ptr< const void > lifetime
std::string id
Definition PlayHost.cpp:108
bool hit
bool found
std::string filter
float dz
float dy
float dx
SimulationTick tick
Bone-bound continuous collision sweeps against World3D (UE5-style trajectory traces).
std::string body
const UnitySourceAsset & source
glm::vec3 point
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
Read-only cross-module contract for sampling named animated attachment points. @ownership Implementat...
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
Strong, domain-neutral reference to a runtime subject.
Definition SubjectRef.h:26
constexpr std::uint64_t value() const noexcept
Returns the underlying value at an explicit protocol boundary.
Box3D rigid-body world. Script coordinates are meters (Box3D native), unlike 2D World which uses pixe...
Definition World3D.h:63
PhysicsWorldHandle runtimeHandle() const noexcept
Process-local identity used by PhysicsLink; invalid after destruction.
Definition World3D.h:141
bool isValid() const
True while the underlying Box3D world is alive.
Definition World3D.cpp:430
Result< void > clearPoseSource(SubjectRef subject)
Remove one subject's pose source and disarm its armed colliders.
Result< void > arm(SubjectRef subject, std::string_view colliderId)
Arm one collider for continuous sweeps while an attack window is open.
Result< TrajectorySample > sampleArmed(SubjectRef subject, std::string_view colliderId) const
Current world-space sample for one armed collider, or NotFound.
Result< void > unregisterCollider(std::string_view colliderId)
Remove one catalog entry and disarm every matching armed window.
TrajectoryCollisionRuntime(World3D &world)
Construct a runtime bound to one borrowed World3D.
Result< TrajectoryFrame > advance(SimulationTick tick)
Sample poses for armed colliders, sweep previous→current through World3D, emit unique hits.
Result< void > registerCollider(BoneColliderDefinition definition)
Register or replace one collider catalog entry after full validation.
Result< void > disarm(SubjectRef subject, std::string_view colliderId)
Disarm one collider and forget its per-target hit memory.
Result< void > bindPoseSource(SubjectRef subject, IAttachmentPointSource &source)
Bind a subject's attachment pose source.
~TrajectoryCollisionRuntime()
Releases TrajectoryCollisionRuntime resources.
double sample(const Heightmap &map, double u, double v)
Sample.
Plain world-space point shared by animation consumers without a math-library dependency.
Query filter applied atomically for one owning query operation.
Catalog entry that binds a sphere or capsule to named attachment points.
std::string endBoneName
Optional second attachment for dual-socket capsules. Empty means a single-bone capsule axis along loc...
std::string boneName
Primary / start attachment name resolved by IAttachmentPointSource.
int maxHits
Maximum World3D cast results retained per armed collider per frame.
std::uint32_t maskBits
Query mask bits applied during sweeps.
float halfHeight
Half-length along local +Y for single-bone capsules; ignored otherwise.
AttachmentPoint localOffset
Local offset on boneName (sphere center or capsule start/center).
float radius
Non-negative finite collision radius in meters.
std::uint32_t categoryBits
Query category bits applied during sweeps.
int ignoredBodyId
Optional body id ignored by sweeps (-1 = none).
AttachmentPoint endLocalOffset
Local offset on endBoneName for dual-socket capsules.
Owning result of one deterministic trajectory advance. @cost Linear in the number of unique World3D c...
std::vector< TrajectoryHit > hits
Unique contacts produced by armed collider sweeps this tick.
World-space sample of one collider endpoint pair.