载入中...
搜索中...
未找到
VolumeFluid.cpp
浏览该文件的文档.
1#include "fluids/VolumeFluidInternal.inc"
2
3namespace eve::fluids {
4VolumeFluid::VolumeFluid(const VolumeFluidSettings& settings) : impl_(std::make_unique<Impl>(settings)) {}
5VolumeFluid::~VolumeFluid() = default;
6
8 const auto extent = s.maximum - s.minimum;
9 const bool valid = s.capacity > 0 && s.capacity <= 1000000 && std::isfinite(s.spacing) && s.spacing >= 0.001f &&
10 s.spacing <= 10.f && finite(s.gravity) && glm::length(s.gravity) <= 1000.f &&
11 finite(s.minimum) && finite(s.maximum) &&
12 glm::all(glm::greaterThan(extent, glm::vec3(s.spacing * 2.f))) && s.iterations > 0 &&
13 s.iterations <= 20;
14 if (!valid)
15 return Result<std::unique_ptr<VolumeFluid>>::failure(Diagnostic::error(
16 DiagnosticCode::InvalidArgument, "Invalid volume-fluid settings", "fluids.volume.settings"));
17 const auto cells = glm::ceil(glm::dvec3(extent) / double(2.f * s.spacing)) + 1.0;
18 if (cells.x * cells.y * cells.z > 2000000.0)
19 return Result<std::unique_ptr<VolumeFluid>>::failure(Diagnostic::error(
20 DiagnosticCode::InvalidArgument, "Volume-fluid grid exceeds 2000000 cells", "fluids.volume.bounds"));
21 return Result<std::unique_ptr<VolumeFluid>>::success(std::unique_ptr<VolumeFluid>(new VolumeFluid(s)));
22}
23
24Result<void> VolumeFluid::emit(std::span<const VolumeFluidParticle> input) {
25 if (input.size() > impl_->settings.capacity - impl_->state.size()) return invalid("Particle capacity exceeded");
26 if (!impl_->colliders.empty() && uint64_t(impl_->state.size() + input.size()) * impl_->colliders.size() > 4000000)
27 return invalid("Analytic collider candidate budget exceeded");
28 if (!impl_->sdfColliders.empty() &&
29 uint64_t(impl_->state.size() + input.size()) * impl_->sdfColliders.size() > 4000000)
30 return invalid("SDF collider candidate budget exceeded");
31 if (!impl_->heightFieldColliders.empty() &&
32 uint64_t(impl_->state.size() + input.size()) * impl_->heightFieldColliders.size() > 4000000)
33 return invalid("Height-field collider candidate budget exceeded");
34 for (const auto& p : input) {
35 if (!finite(p.position) || !finite(p.velocity) || !finite(p.angularVelocity) || !finite(p.color) ||
36 !finite(p.data) || !validMaterial(p.material) || !validParticleShape(p) || !range(p.life, 0.f, 86400.f) ||
37 !validFilter(p.collisionFilter) || p.actorGroup > 0x00ffffffu || glm::length(p.velocity) > 100.f ||
38 glm::length(p.angularVelocity) > 1000.f ||
39 glm::any(glm::notEqual(impl_->constrain(p.position), p.position)))
40 return invalid("Invalid emitted particle");
41 }
42 const size_t first = impl_->state.size();
43 impl_->state.insert(impl_->state.end(), input.begin(), input.end());
44 for (size_t i = first; i < impl_->state.size(); ++i) {
45 auto& particle = impl_->state[i];
46 if (glm::all(glm::equal(particle.radii, glm::vec3(0.f))))
47 particle.radii = glm::vec3(impl_->settings.spacing * .5f);
48 particle.orientation = glm::normalize(particle.orientation);
49 impl_->renderPrevious.push_back(particle.position);
50 impl_->grabberLabels.push_back(Impl::noGrabber);
51 impl_->grabberLocalPositions.push_back(glm::vec3(0.f));
52 impl_->recordParticleEvent(VolumeFluidParticleEventType::Emitted, unsigned(i), particle);
53 }
54 impl_->winds.clear();
55 impl_->externalForces.clear();
56 return Result<void>::success();
57}
58
59Result<void> VolumeFluid::configureParticleEvents(unsigned capacity) {
60 if (capacity > 65536) return invalid("Particle event capacity exceeds 65536");
61 std::vector<VolumeFluidParticleEvent> replacement;
62 replacement.reserve(capacity);
63 impl_->particleEvents.swap(replacement);
64 impl_->particleEventCapacity = capacity;
65 impl_->droppedParticleEvents = 0;
66 return Result<void>::success();
67}
68
69VolumeFluidParticleEventBatch VolumeFluid::drainParticleEvents() {
71 batch.events = impl_->particleEvents;
72 batch.dropped = impl_->droppedParticleEvents;
73 impl_->particleEvents.clear();
74 impl_->droppedParticleEvents = 0;
75 return batch;
76}
77
78Result<void> VolumeFluid::step(float seconds, unsigned substeps) {
79 return stepWithThermalContacts(seconds, substeps, {});
80}
81
82Result<void> VolumeFluid::setParticleWinds(std::span<const glm::vec3> winds) {
83 if (winds.empty()) {
84 impl_->winds.clear();
85 return Result<void>::success();
86 }
87 if (winds.size() != impl_->state.size() || std::any_of(winds.begin(), winds.end(), [](glm::vec3 wind) {
88 return !finite(wind) || glm::length(wind) > 1000.f;
89 }))
90 return invalid("Invalid per-particle wind count or velocity");
91 impl_->winds.assign(winds.begin(), winds.end());
92 return Result<void>::success();
93}
94
95Result<void> VolumeFluid::setParticleExternalForces(std::span<const glm::vec3> forces) {
96 if (forces.empty()) {
97 impl_->externalForces.clear();
98 return Result<void>::success();
99 }
100 if (forces.size() != impl_->state.size() || std::any_of(forces.begin(), forces.end(), [](glm::vec3 force) {
101 return !finite(force) || glm::length(force) > 1000.f;
102 }))
103 return invalid("Invalid per-particle external force count or magnitude");
104 impl_->externalForces.assign(forces.begin(), forces.end());
105 return Result<void>::success();
106}
107
108Result<void> VolumeFluid::accumulateWindZones(std::span<const VolumeFluidWindZone> zones, float fixedTimeSeconds) {
109 return accumulateForceZones(zones, fixedTimeSeconds, true);
110}
111
112Result<void> VolumeFluid::accumulateExternalForceZones(std::span<const VolumeFluidWindZone> zones,
113 float fixedTimeSeconds) {
114 return accumulateForceZones(zones, fixedTimeSeconds, false);
115}
116
117Result<void> VolumeFluid::accumulateForceZones(std::span<const VolumeFluidWindZone> zones, float fixedTimeSeconds,
118 bool asWind) {
119 constexpr size_t kMaxZones = 64;
120 constexpr size_t kCandidateBudget = 4000000;
121 if (zones.size() > kMaxZones || !std::isfinite(fixedTimeSeconds) || fixedTimeSeconds < 0.f ||
122 fixedTimeSeconds > 1e9f)
123 return invalid("Invalid wind-zone count or fixed time");
124 if (zones.empty() || impl_->state.empty()) return Result<void>::success();
125
126 size_t sphericalCount = 0;
127 glm::vec3 ambient(0.f);
128 std::vector<float> resolvedIntensity;
129 resolvedIntensity.reserve(zones.size());
130 for (const auto& zone : zones) {
131 const bool validType =
132 zone.type == VolumeFluidWindZoneType::Ambient || zone.type == VolumeFluidWindZoneType::Spherical;
133 if (!validType || !finite(zone.center) || !finite(zone.direction) || glm::length(zone.direction) > 1000.f ||
134 !range(zone.intensity, -1000.f, 1000.f) || !range(zone.turbulence, -1000.f, 1000.f) ||
135 !range(zone.turbulenceFrequency, 0.f, 1000.f) || !range(zone.turbulenceSeed, -1000000.f, 1000000.f) ||
136 (zone.type == VolumeFluidWindZoneType::Spherical && !range(zone.radius, .001f, 10000.f)))
137 return invalid("Invalid wind-zone field");
138 const float noise = std::clamp(
139 .5f + .5f * glm::perlin(glm::vec2(fixedTimeSeconds * zone.turbulenceFrequency, zone.turbulenceSeed)), 0.f,
140 1.f);
141 const float intensity = zone.intensity + noise * zone.turbulence;
142 resolvedIntensity.push_back(intensity);
143 if (zone.type == VolumeFluidWindZoneType::Ambient)
144 ambient += zone.direction * intensity;
145 else
146 ++sphericalCount;
147 }
148 if (sphericalCount != 0 && impl_->state.size() > kCandidateBudget / sphericalCount)
149 return invalid("Wind-zone candidate budget exceeded");
150
151 auto& current = asWind ? impl_->winds : impl_->externalForces;
152 auto& candidate = asWind ? impl_->windScratch : impl_->externalForceScratch;
153 if (current.empty())
154 candidate.assign(impl_->state.size(), glm::vec3(0.f));
155 else
156 candidate.assign(current.begin(), current.end());
157 for (size_t particleIndex = 0; particleIndex < impl_->state.size(); ++particleIndex) {
158 auto force = candidate[particleIndex] + ambient;
159 for (size_t zoneIndex = 0; zoneIndex < zones.size(); ++zoneIndex) {
160 const auto& zone = zones[zoneIndex];
161 if (zone.type != VolumeFluidWindZoneType::Spherical) continue;
162 const auto distance = impl_->state[particleIndex].position - zone.center;
163 const float squaredDistance = glm::dot(distance, distance);
164 const float squaredRadius = zone.radius * zone.radius;
165 if (squaredDistance >= squaredRadius) continue;
166 const float falloff = std::clamp((squaredRadius - squaredDistance) / squaredRadius, 0.f, 1.f);
167 if (zone.radial) {
168 force += distance / (std::sqrt(squaredDistance) + std::numeric_limits<float>::epsilon()) * falloff *
169 resolvedIntensity[zoneIndex];
170 } else {
171 force += zone.direction * falloff * resolvedIntensity[zoneIndex];
172 }
173 }
174 if (!finite(force) || glm::length(force) > 1000.f)
175 return invalid(asWind ? "Accumulated wind-zone velocity exceeds limit"
176 : "Accumulated external force exceeds limit");
177 candidate[particleIndex] = force;
178 }
179 current.swap(candidate);
180 return Result<void>::success();
181}
182
183Result<void> VolumeFluid::stepWithThermalContacts(float seconds, unsigned substeps,
184 std::span<const VolumeFluidThermalRule> rules) {
185 EV_PROFILE_MODULE("fluids", "VolumeFluid::step");
186 if (!std::isfinite(seconds) || seconds <= 0.f || seconds > 1.f / 30.f || substeps == 0 || substeps > 32 ||
187 seconds / float(substeps) > 1.f / 120.f)
188 return invalid("Invalid timestep or substep count");
189 if (rules.size() > 1024) return invalid("Too many thermal rules");
190 auto& ordered = impl_->thermalRulesScratch;
191 ordered.assign(rules.begin(), rules.end());
192 std::sort(ordered.begin(), ordered.end(),
193 [](const auto& a, const auto& b) { return a.colliderLabel < b.colliderLabel; });
194 for (size_t i = 0; i < ordered.size(); ++i) {
195 const auto& rule = ordered[i];
196 if (!range(rule.rate, -1000000.f, 1000000.f) || !range(rule.minimumViscosity, 0.f, 100.f) ||
197 !range(rule.maximumViscosity, rule.minimumViscosity, 100.f) || !range(rule.minimumCohesion, 0.f, 10.f) ||
198 !range(rule.maximumCohesion, rule.minimumCohesion, 10.f) ||
199 (i > 0 && ordered[i - 1].colliderLabel == rule.colliderLabel) ||
200 std::none_of(impl_->colliders.begin(), impl_->colliders.end(),
201 [&](const auto& c) { return c.label == rule.colliderLabel; }))
202 return invalid("Invalid thermal rule or collider label");
203 }
204 auto& contactParticles = impl_->thermalContactParticles;
205 contactParticles.clear();
206 struct Motion {
207 size_t particle;
208 glm::vec3 start, target, velocity;
209 glm::quat startOrientation, targetOrientation;
210 glm::vec3 localPosition;
211 glm::quat localOrientation;
212 unsigned colliderLabel;
213 size_t colliderIndex;
214 glm::vec3 inertialReaction;
215 glm::vec3 angularVelocity, angularReaction;
216 bool constrainOrientation;
217 bool dynamic;
218 float compliance, breakThreshold;
219 glm::vec3 constraintReaction{0.f};
220 glm::vec3 dynamicAngularReaction{0.f};
221 bool broken = false;
222 };
223 std::vector<Motion> motions;
224 if (!impl_->attachments.empty()) {
225 std::vector<std::pair<unsigned, size_t>> labels;
226 labels.reserve(impl_->colliders.size());
227 for (size_t i = 0; i < impl_->colliders.size(); ++i) labels.emplace_back(impl_->colliders[i].label, i);
228 std::sort(labels.begin(), labels.end());
229 motions.reserve(impl_->attachments.size());
230 for (const auto& attachment : impl_->attachments) {
231 const auto found = std::lower_bound(labels.begin(), labels.end(),
232 std::pair<unsigned, size_t>{attachment.colliderLabel, 0});
233 if (found == labels.end() || found->first != attachment.colliderLabel)
234 return invalid("Missing solid attachment collider");
235 const auto target = impl_->colliders[found->second].center +
236 impl_->colliderRotations[found->second] * attachment.localPosition;
237 const auto start = impl_->state[attachment.particleIndex].position;
238 const auto velocity = (target - start) / seconds;
239 const auto& collider = impl_->colliders[found->second];
240 const auto colliderQ =
241 glm::quat(collider.rotation.w, collider.rotation.x, collider.rotation.y, collider.rotation.z);
242 const auto localQ = glm::quat(attachment.localOrientation.w, attachment.localOrientation.x,
243 attachment.localOrientation.y, attachment.localOrientation.z);
244 const auto startQ = particleQuaternion(impl_->state[attachment.particleIndex]);
245 const auto targetQ = glm::normalize(colliderQ * localQ);
246 if (!finite(target) || !finite(velocity) || glm::length(velocity) > 100.f ||
247 glm::any(glm::notEqual(impl_->constrain(target), target)))
248 return invalid("Solid attachment motion exceeds bounds or speed");
249 const auto& particle = impl_->state[attachment.particleIndex];
250 const float mass = impl_->volume * particle.material.density;
251 const auto unconstrainedVelocity =
252 particle.velocity + impl_->settings.gravity * particle.material.buoyancy * seconds;
253 const auto targetAngular = collider.angularVelocity;
254 const auto radii = particle.radii;
255 const glm::vec3 principal =
256 mass * .2f *
257 glm::vec3(radii.y * radii.y + radii.z * radii.z, radii.x * radii.x + radii.z * radii.z,
258 radii.x * radii.x + radii.y * radii.y);
259 const auto orientation = glm::mat3_cast(startQ);
260 const auto angularReaction =
261 -orientation * glm::mat3(principal.x, 0.f, 0.f, 0.f, principal.y, 0.f, 0.f, 0.f, principal.z) *
262 glm::transpose(orientation) * (targetAngular - particle.angularVelocity);
263 motions.push_back(
264 {attachment.particleIndex, start, target, velocity, startQ, targetQ, attachment.localPosition, localQ,
265 attachment.colliderLabel, found->second,
266 attachment.dynamic ? glm::vec3(0.f) : -mass * (velocity - unconstrainedVelocity),
267 attachment.constrainOrientation ? targetAngular : particle.angularVelocity,
268 !attachment.dynamic && attachment.constrainOrientation ? angularReaction : glm::vec3(0.f),
269 attachment.constrainOrientation, attachment.dynamic, attachment.compliance,
270 attachment.breakThreshold});
271 }
272 }
273 auto& colliderEnds = impl_->colliderPoseScratch;
274 auto& sdfColliderEnds = impl_->sdfColliderPoseScratch;
275 auto& heightFieldEnds = impl_->heightFieldPoseScratch;
276 colliderEnds = impl_->colliders;
277 sdfColliderEnds.resize(impl_->sdfColliders.size());
278 for (size_t i = 0; i < impl_->sdfColliders.size(); ++i) {
279 const auto& collider = impl_->sdfColliders[i];
280 sdfColliderEnds[i] = {collider.label, collider.position, collider.rotation,
281 collider.scale, collider.velocity, collider.angularVelocity};
282 }
283 heightFieldEnds.resize(impl_->heightFieldColliders.size());
284 for (size_t i = 0; i < impl_->heightFieldColliders.size(); ++i) {
285 const auto& collider = impl_->heightFieldColliders[i];
286 heightFieldEnds[i] = {collider.label, collider.position, collider.rotation, 1.f,
287 collider.velocity, collider.angularVelocity};
288 }
289 auto interpolatedRotation = [](glm::vec4 rotation, glm::vec3 angularVelocity, float seconds, float fraction) {
290 const auto end = glm::normalize(glm::quat(rotation.w, rotation.x, rotation.y, rotation.z));
291 const float speed = glm::length(angularVelocity);
292 if (speed <= 1e-7f) return end;
293 return glm::normalize(glm::angleAxis(-speed * seconds * (1.f - fraction), angularVelocity / speed) * end);
294 };
295 for (size_t i = 0; i < sdfColliderEnds.size(); ++i)
296 if (impl_->sdfColliders[i].inverted) {
297 const auto& pose = sdfColliderEnds[i];
298 const auto& sdf = impl_->sdfColliders[i].sdf;
299 const auto startRotation =
300 glm::mat3_cast(interpolatedRotation(pose.rotation, pose.angularVelocity, seconds, 0.f));
301 const auto startPosition = pose.position - pose.velocity * seconds;
302 const auto minimum = sdf.origin;
303 const auto maximum = minimum + glm::vec3(sdf.dims - glm::ivec3(1)) * sdf.cellSize;
304 for (unsigned corner = 0; corner < 8; ++corner) {
305 const glm::vec3 world{corner & 1 ? impl_->settings.maximum.x : impl_->settings.minimum.x,
306 corner & 2 ? impl_->settings.maximum.y : impl_->settings.minimum.y,
307 corner & 4 ? impl_->settings.maximum.z : impl_->settings.minimum.z};
308 const auto local = glm::transpose(startRotation) * (world - startPosition) / pose.scale;
309 if (glm::any(glm::lessThan(local, minimum)) || glm::any(glm::greaterThan(local, maximum)))
310 return invalid("Moving inverted SDF domain must contain solver bounds for the complete step");
311 }
312 }
313 const auto restoreColliderEnds = [&]() {
314 impl_->colliders.swap(colliderEnds);
315 for (size_t i = 0; i < sdfColliderEnds.size(); ++i) {
316 impl_->sdfColliders[i].position = sdfColliderEnds[i].position;
317 impl_->sdfColliders[i].rotation = sdfColliderEnds[i].rotation;
318 }
319 for (size_t i = 0; i < heightFieldEnds.size(); ++i) {
320 impl_->heightFieldColliders[i].position = heightFieldEnds[i].position;
321 impl_->heightFieldColliders[i].rotation = heightFieldEnds[i].rotation;
322 }
323 impl_->colliderRotations.resize(impl_->colliders.size());
324 for (size_t i = 0; i < impl_->colliders.size(); ++i) {
325 const auto& r = impl_->colliders[i].rotation;
326 impl_->colliderRotations[i] = glm::mat3_cast(glm::normalize(glm::quat(r.w, r.x, r.y, r.z)));
327 }
328 impl_->sdfColliderRotations.resize(impl_->sdfColliders.size());
329 for (size_t i = 0; i < impl_->sdfColliders.size(); ++i) {
330 const auto& r = impl_->sdfColliders[i].rotation;
331 impl_->sdfColliderRotations[i] = glm::mat3_cast(glm::normalize(glm::quat(r.w, r.x, r.y, r.z)));
332 }
333 impl_->heightFieldColliderRotations.resize(impl_->heightFieldColliders.size());
334 for (size_t i = 0; i < impl_->heightFieldColliders.size(); ++i) {
335 const auto& r = impl_->heightFieldColliders[i].rotation;
336 impl_->heightFieldColliderRotations[i] = glm::mat3_cast(glm::normalize(glm::quat(r.w, r.x, r.y, r.z)));
337 }
338 };
339 auto& original = impl_->rollbackState;
340 original = impl_->state;
341 impl_->renderPreviousScratch.resize(impl_->state.size());
342 for (size_t i = 0; i < impl_->state.size(); ++i) impl_->renderPreviousScratch[i] = impl_->state[i].position;
343 auto& originalContacts = impl_->rollbackContacts;
344 originalContacts = impl_->contacts;
345 auto& originalAttachments = impl_->rollbackAttachments;
346 originalAttachments = impl_->attachments;
347 impl_->attachmentLabels.clear();
348 impl_->attachmentOrientationConstraints.clear();
349 impl_->attachmentStressImpulse.clear();
350 if (!motions.empty()) {
351 impl_->attachmentLabels.assign(impl_->state.size(), -1);
352 impl_->attachmentOrientationConstraints.assign(impl_->state.size(), uint8_t(0));
353 impl_->attachmentStressImpulse.assign(impl_->state.size(), glm::vec3(0.f));
354 for (const auto& motion : motions) {
355 if (motion.dynamic) continue;
356 impl_->attachmentLabels[motion.particle] = int(motion.colliderLabel);
357 impl_->attachmentOrientationConstraints[motion.particle] = motion.constrainOrientation ? 1 : 0;
358 }
359 }
360 impl_->contacts.clear();
361 for (auto& p : impl_->state)
362 if (p.material.phase == VolumeFluidPhase::Solid) {
363 p.velocity = glm::vec3(0.f);
364 p.angularVelocity = glm::vec3(0.f);
365 }
366 for (unsigned k = 0; k < substeps; ++k) {
367 const float fraction = float(k + 1) / float(substeps);
368 for (size_t i = 0; i < colliderEnds.size(); ++i) {
369 const auto& end = colliderEnds[i];
370 auto& sample = impl_->colliders[i];
371 sample.center = end.center - end.velocity * (seconds * (1.f - fraction));
372 const auto q = interpolatedRotation(end.rotation, end.angularVelocity, seconds, fraction);
373 sample.rotation = {q.x, q.y, q.z, q.w};
374 impl_->colliderRotations[i] = glm::mat3_cast(q);
375 }
376 for (size_t i = 0; i < sdfColliderEnds.size(); ++i) {
377 const auto& end = sdfColliderEnds[i];
378 auto& sample = impl_->sdfColliders[i];
379 sample.position = end.position - end.velocity * (seconds * (1.f - fraction));
380 const auto q = interpolatedRotation(end.rotation, end.angularVelocity, seconds, fraction);
381 sample.rotation = {q.x, q.y, q.z, q.w};
382 impl_->sdfColliderRotations[i] = glm::mat3_cast(q);
383 }
384 for (size_t i = 0; i < heightFieldEnds.size(); ++i) {
385 const auto& end = heightFieldEnds[i];
386 auto& sample = impl_->heightFieldColliders[i];
387 sample.position = end.position - end.velocity * (seconds * (1.f - fraction));
388 const auto q = interpolatedRotation(end.rotation, end.angularVelocity, seconds, fraction);
389 sample.rotation = {q.x, q.y, q.z, q.w};
390 impl_->heightFieldColliderRotations[i] = glm::mat3_cast(q);
391 }
392 for (const auto& motion : motions) {
393 if (motion.dynamic) continue;
394 auto& p = impl_->state[motion.particle];
395 p.position = glm::mix(motion.start, motion.target, float(k + 1) / float(substeps));
396 p.velocity = p.material.phase == VolumeFluidPhase::Solid ? motion.velocity : glm::vec3(0.f);
397 if (motion.constrainOrientation) {
398 p.angularVelocity = motion.angularVelocity;
399 const auto orientation = glm::normalize(
400 glm::slerp(motion.startOrientation, motion.targetOrientation, float(k + 1) / float(substeps)));
401 p.orientation = {orientation.x, orientation.y, orientation.z, orientation.w};
402 }
403 }
404 if (motions.empty()) {
405 if (impl_->externalForces.empty())
406 impl_->substep<false, false>(seconds / float(substeps), rules.empty() ? nullptr : &contactParticles);
407 else
408 impl_->substep<false, true>(seconds / float(substeps), rules.empty() ? nullptr : &contactParticles);
409 } else if (impl_->externalForces.empty()) {
410 impl_->substep<true, false>(seconds / float(substeps), rules.empty() ? nullptr : &contactParticles);
411 } else {
412 impl_->substep<true, true>(seconds / float(substeps), rules.empty() ? nullptr : &contactParticles);
413 }
414 if (!impl_->stitches.empty()) {
415 const float stitchDt = seconds / float(substeps);
416 impl_->stitchStart.resize(impl_->state.size());
417 impl_->stitchDelta.resize(impl_->state.size());
418 impl_->stitchCounts.resize(impl_->state.size());
419 impl_->stitchTouched.clear();
420 impl_->stitchTouched.reserve(impl_->stitches.size() * 2);
421 for (const auto& stitch : impl_->stitches) {
422 impl_->stitchTouched.push_back(stitch.particleIndex1);
423 impl_->stitchTouched.push_back(stitch.particleIndex2);
424 impl_->stitchStart[stitch.particleIndex1] = impl_->state[stitch.particleIndex1].position;
425 impl_->stitchStart[stitch.particleIndex2] = impl_->state[stitch.particleIndex2].position;
426 }
427 std::sort(impl_->stitchTouched.begin(), impl_->stitchTouched.end());
428 impl_->stitchTouched.erase(std::unique(impl_->stitchTouched.begin(), impl_->stitchTouched.end()),
429 impl_->stitchTouched.end());
430 impl_->stitchLambdas.assign(impl_->stitches.size(), 0.f);
431 for (unsigned iteration = 0; iteration < impl_->settings.iterations; ++iteration) {
432 for (const unsigned index : impl_->stitchTouched) {
433 impl_->stitchDelta[index] = glm::vec3(0.f);
434 impl_->stitchCounts[index] = 0;
435 }
436 for (size_t stitchIndex = 0; stitchIndex < impl_->stitches.size(); ++stitchIndex) {
437 const auto& stitch = impl_->stitches[stitchIndex];
438 const auto fixed = [&](unsigned index) {
439 return impl_->state[index].material.phase == VolumeFluidPhase::Solid ||
440 impl_->grabberLabels[index] != Impl::noGrabber ||
441 (!impl_->attachmentLabels.empty() && impl_->attachmentLabels[index] >= 0);
442 };
443 const float w1 = fixed(stitch.particleIndex1)
444 ? 0.f
445 : 1.f / (impl_->volume * impl_->state[stitch.particleIndex1].material.density);
446 const float w2 = fixed(stitch.particleIndex2)
447 ? 0.f
448 : 1.f / (impl_->volume * impl_->state[stitch.particleIndex2].material.density);
449 const auto distance =
450 impl_->state[stitch.particleIndex1].position - impl_->state[stitch.particleIndex2].position;
451 const float length = glm::length(distance);
452 const float alpha = stitch.compliance / (stitchDt * stitchDt);
453 const float deltaLambda = (-length - alpha * impl_->stitchLambdas[stitchIndex]) /
454 (w1 + w2 + alpha + std::numeric_limits<float>::epsilon());
455 const auto delta = deltaLambda * distance / (length + std::numeric_limits<float>::epsilon());
456 impl_->stitchDelta[stitch.particleIndex1] += delta * w1;
457 impl_->stitchDelta[stitch.particleIndex2] -= delta * w2;
458 ++impl_->stitchCounts[stitch.particleIndex1];
459 ++impl_->stitchCounts[stitch.particleIndex2];
460 impl_->stitchLambdas[stitchIndex] += deltaLambda;
461 }
462 for (const unsigned index : impl_->stitchTouched)
463 if (impl_->stitchCounts[index] != 0)
464 impl_->state[index].position += impl_->stitchDelta[index] / float(impl_->stitchCounts[index]);
465 }
466 for (const unsigned index : impl_->stitchTouched)
467 if (impl_->stitchCounts[index] != 0)
468 impl_->state[index].velocity +=
469 (impl_->state[index].position - impl_->stitchStart[index]) / stitchDt;
470 }
471 const float substepSeconds = seconds / float(substeps);
472 for (auto& motion : motions) {
473 if (!motion.dynamic || motion.broken) continue;
474 auto& particle = impl_->state[motion.particle];
475 const auto& collider = impl_->colliders[motion.colliderIndex];
476 const auto& rotation = impl_->colliderRotations[motion.colliderIndex];
477 const auto target = collider.center + rotation * motion.localPosition;
478 const float mass = impl_->volume * particle.material.density;
479 const float inverseMass = 1.f / mass;
480 const float alpha = motion.compliance / (substepSeconds * substepSeconds);
481 const float response = inverseMass / (inverseMass + alpha);
482 const auto correction = (target - particle.position) * response;
483 particle.position += correction;
484 particle.velocity += correction / substepSeconds;
485 motion.constraintReaction -= mass * correction / substepSeconds;
486 float force = mass * glm::length(correction) / (substepSeconds * substepSeconds);
487
488 if (motion.constrainOrientation) {
489 const auto colliderQ =
490 glm::quat(collider.rotation.w, collider.rotation.x, collider.rotation.y, collider.rotation.z);
491 const auto currentQ = particleQuaternion(particle);
492 const auto desiredQ = glm::normalize(colliderQ * motion.localOrientation);
493 const auto nextQ = glm::normalize(glm::slerp(currentQ, desiredQ, response));
494 auto deltaQ = glm::normalize(nextQ * glm::conjugate(currentQ));
495 if (deltaQ.w < 0.f) deltaQ = -deltaQ;
496 const float angle = 2.f * std::acos(std::clamp(deltaQ.w, -1.f, 1.f));
497 glm::vec3 deltaAngular(0.f);
498 const float sine = std::sqrt(std::max(0.f, 1.f - deltaQ.w * deltaQ.w));
499 if (sine > 1e-6f)
500 deltaAngular = glm::vec3(deltaQ.x, deltaQ.y, deltaQ.z) * (angle / sine / substepSeconds);
501 particle.orientation = {nextQ.x, nextQ.y, nextQ.z, nextQ.w};
502 particle.angularVelocity += deltaAngular;
503 const auto radii = particle.radii;
504 const float inertia =
505 mass * .2f *
506 ((radii.y * radii.y + radii.z * radii.z) + (radii.x * radii.x + radii.z * radii.z) +
507 (radii.x * radii.x + radii.y * radii.y)) /
508 3.f;
509 motion.dynamicAngularReaction -= deltaAngular * inertia;
510 }
511 if (force > motion.breakThreshold) motion.broken = true;
512 }
513 for (const auto& p : impl_->state)
514 if (!finite(p.position) || !finite(p.velocity) || !finite(p.angularVelocity) ||
515 glm::length(p.angularVelocity) > 1000.f || !finite(p.color) || !finite(p.data) ||
516 (p.material.phase == VolumeFluidPhase::Solid &&
517 glm::any(glm::notEqual(impl_->constrain(p.position), p.position)))) {
518 impl_->state = original;
519 impl_->contacts = originalContacts;
520 impl_->attachments = originalAttachments;
521 restoreColliderEnds();
522 return Result<void>::failure(Diagnostic::error(
523 DiagnosticCode::Failed, "Nonfinite state or solid outside bounds; step rolled back",
524 "fluids.volume.step"));
525 }
526 }
527 restoreColliderEnds();
528 if (std::any_of(motions.begin(), motions.end(), [](const auto& motion) { return motion.broken; })) {
529 std::erase_if(impl_->attachments, [&](const auto& attachment) {
530 return std::any_of(motions.begin(), motions.end(), [&](const auto& motion) {
531 return motion.broken && motion.particle == attachment.particleIndex &&
532 motion.colliderLabel == attachment.colliderLabel;
533 });
534 });
535 }
536 // The final substep's grid still describes these unchanged positions.
537 auto propagated = impl_->propagateSolidification();
538 if (!propagated) {
539 impl_->state = original;
540 impl_->contacts = originalContacts;
541 impl_->attachments = originalAttachments;
542 return propagated;
543 }
544 impl_->attachmentReactionScratch.clear();
545 impl_->attachmentReactionScratch.reserve(motions.size());
546 for (const auto& motion : motions) {
547 const auto impulse =
548 motion.inertialReaction + motion.constraintReaction + impl_->attachmentStressImpulse[motion.particle];
549 const auto angularImpulse = motion.angularReaction + motion.dynamicAngularReaction;
550 if (glm::dot(impulse, impulse) > 0.f || glm::dot(angularImpulse, angularImpulse) > 0.f)
551 impl_->attachmentReactionScratch.push_back({motion.colliderLabel, motion.target, impulse, angularImpulse});
552 }
553 impl_->attachmentReactions.swap(impl_->attachmentReactionScratch);
554 auto& hits = impl_->thermalHitScratch;
555 hits.clear();
556 hits.reserve(contactParticles.size());
557 for (size_t i = 0; i < contactParticles.size(); ++i)
558 hits.push_back((uint64_t(impl_->contacts[i].colliderLabel) << 32u) | uint64_t(contactParticles[i]));
559 std::sort(hits.begin(), hits.end());
560 hits.erase(std::unique(hits.begin(), hits.end()), hits.end());
561 for (const uint64_t hit : hits) {
562 const unsigned label = unsigned(hit >> 32u);
563 const auto rule = std::lower_bound(ordered.begin(), ordered.end(), label,
564 [](const auto& item, unsigned value) { return item.colliderLabel < value; });
565 if (rule == ordered.end() || rule->colliderLabel != label) continue;
566 auto& data = impl_->state[size_t(hit & 0xFFFFFFFFull)].data;
567 data.x = std::clamp(data.x + rule->rate * seconds, rule->minimumViscosity, rule->maximumViscosity);
568 data.y = std::clamp(data.y + rule->rate * seconds, rule->minimumCohesion, rule->maximumCohesion);
569 }
570 // Reuse the neighbor-link scratch as the old-to-new index map after stepping.
571 impl_->next.resize(impl_->state.size());
572 impl_->grabberLabelScratch.resize(impl_->state.size());
573 impl_->grabberLocalScratch.resize(impl_->state.size());
574 size_t remaining = 0;
575 for (size_t i = 0; i < impl_->state.size(); ++i) {
576 auto& p = impl_->state[i];
577 const bool expires = p.life > 0.f && (p.life -= seconds) <= 0.f;
578 impl_->next[i] = expires ? -1 : int(remaining);
579 if (expires) impl_->recordParticleEvent(VolumeFluidParticleEventType::Killed, unsigned(i), p);
580 if (!expires) {
581 if (remaining != i) impl_->state[remaining] = p;
582 impl_->renderPreviousScratch[remaining] = impl_->renderPreviousScratch[i];
583 impl_->grabberLabelScratch[remaining] = impl_->grabberLabels[i];
584 impl_->grabberLocalScratch[remaining] = impl_->grabberLocalPositions[i];
585 ++remaining;
586 }
587 }
588 impl_->state.resize(remaining);
589 impl_->renderPreviousScratch.resize(remaining);
590 impl_->renderPrevious.swap(impl_->renderPreviousScratch);
591 impl_->grabberLabelScratch.resize(remaining);
592 impl_->grabberLabels.swap(impl_->grabberLabelScratch);
593 impl_->grabberLocalScratch.resize(remaining);
594 impl_->grabberLocalPositions.swap(impl_->grabberLocalScratch);
595 std::erase_if(impl_->attachments, [&](auto& a) {
596 const int mapped = impl_->next[a.particleIndex];
597 if (mapped < 0) return true;
598 a.particleIndex = unsigned(mapped);
599 return false;
600 });
601 std::erase_if(impl_->stitches, [&](auto& stitch) {
602 const int first = impl_->next[stitch.particleIndex1];
603 const int second = impl_->next[stitch.particleIndex2];
604 if (first < 0 || second < 0) return true;
605 stitch.particleIndex1 = unsigned(first);
606 stitch.particleIndex2 = unsigned(second);
607 return false;
608 });
609 std::erase_if(impl_->simplexes, [&](auto& simplex) {
610 for (unsigned i = 0; i < simplex.size; ++i) {
611 const int mapped = impl_->next[simplex.particleIndices[i]];
612 if (mapped < 0) return true;
613 simplex.particleIndices[i] = unsigned(mapped);
614 }
615 return false;
616 });
617 impl_->winds.clear();
618 impl_->externalForces.clear();
619 return Result<void>::success();
620}
621
622std::vector<VolumeFluidParticle> VolumeFluid::particles() const { return impl_->state; }
623std::span<const VolumeFluidParticle> VolumeFluid::particleView() const { return impl_->state; }
624Result<void> VolumeFluid::stepWithColliders(float seconds, unsigned substeps,
625 std::span<const VolumeFluidCollider> colliders,
626 std::span<const VolumeFluidThermalRule> rules) {
627 auto previousColliders = impl_->colliders;
628 auto previousRotations = impl_->colliderRotations;
629 auto previousAttachments = impl_->attachments;
630 auto configured = setColliders(colliders);
631 if (!configured) return configured;
632 auto advanced = stepWithThermalContacts(seconds, substeps, rules);
633 if (!advanced) {
634 impl_->colliders.swap(previousColliders);
635 impl_->colliderRotations.swap(previousRotations);
636 impl_->attachments.swap(previousAttachments);
637 }
638 return advanced;
639}
640size_t VolumeFluid::particleCount() const { return impl_->state.size(); }
641size_t VolumeFluid::availableCapacity() const { return impl_->settings.capacity - impl_->state.size(); }
642float VolumeFluid::spacing() const { return impl_->settings.spacing; }
643Result<void> VolumeFluid::setGravity(glm::vec3 gravity) {
644 if (!finite(gravity) || glm::length(gravity) > 1000.f) return invalid("Invalid world-space gravity");
645 impl_->settings.gravity = gravity;
646 return Result<void>::success();
647}
648glm::vec3 VolumeFluid::gravity() const { return impl_->settings.gravity; }
649Result<void> VolumeFluid::copyInterpolatedPositions(float alpha, std::vector<glm::vec3>& positions) const {
650 if (!std::isfinite(alpha) || alpha < 0.f || alpha > 1.f) return invalid("Invalid render interpolation alpha");
651 positions.resize(impl_->state.size());
652 for (size_t i = 0; i < impl_->state.size(); ++i) positions[i] = interpolatedPositionUnchecked(i, alpha);
653 return Result<void>::success();
654}
655Result<float> VolumeFluid::copyInterpolatedRenderData(float alpha, std::vector<glm::vec3>& positions,
656 std::vector<glm::vec4>& colors) const {
657 if (!std::isfinite(alpha) || alpha < 0.f || alpha > 1.f)
658 return Result<float>::failure(invalid("Invalid render interpolation alpha").status());
659 positions.resize(impl_->state.size());
660 colors.resize(impl_->state.size());
661 for (size_t i = 0; i < impl_->state.size(); ++i) {
662 positions[i] = interpolatedPositionUnchecked(i, alpha);
663 colors[i] = impl_->state[i].color;
664 }
665 return Result<float>::success(impl_->settings.spacing);
666}
667glm::vec3 VolumeFluid::interpolatedPositionUnchecked(size_t particleIndex, float alpha) const {
668 return glm::mix(impl_->renderPrevious[particleIndex], impl_->state[particleIndex].position, alpha);
669}
670float VolumeFluid::copyRenderData(std::vector<glm::vec3>& positions, std::vector<glm::vec4>& colors) const {
671 positions.resize(impl_->state.size());
672 colors.resize(impl_->state.size());
673 for (size_t i = 0; i < impl_->state.size(); ++i) {
674 positions[i] = impl_->state[i].position;
675 colors[i] = impl_->state[i].color;
676 }
677 return impl_->settings.spacing;
678}
679float VolumeFluid::copySurfaceRenderData(std::vector<glm::vec3>& positions, std::vector<glm::vec4>& colors,
680 std::vector<glm::vec3>& radii, std::vector<glm::vec4>& orientations) const {
681 const size_t count = impl_->state.size();
682 positions.resize(count);
683 colors.resize(count);
684 radii.resize(count);
685 orientations.resize(count);
686 for (size_t i = 0; i < count; ++i) {
687 const auto& particle = impl_->state[i];
688 positions[i] = particle.position;
689 colors[i] = particle.color;
690 radii[i] = particle.radii;
691 orientations[i] = particle.orientation;
692 }
693 return impl_->settings.spacing;
694}
695Result<float> VolumeFluid::copyInterpolatedSurfaceRenderData(float alpha, std::vector<glm::vec3>& positions,
696 std::vector<glm::vec4>& colors,
697 std::vector<glm::vec3>& radii,
698 std::vector<glm::vec4>& orientations) const {
699 if (!std::isfinite(alpha) || alpha < 0.f || alpha > 1.f)
700 return Result<float>::failure(invalid("Invalid render interpolation alpha").status());
701 const size_t count = impl_->state.size();
702 positions.resize(count);
703 colors.resize(count);
704 radii.resize(count);
705 orientations.resize(count);
706 for (size_t i = 0; i < count; ++i) {
707 const auto& particle = impl_->state[i];
708 positions[i] = interpolatedPositionUnchecked(i, alpha);
709 colors[i] = particle.color;
710 radii[i] = particle.radii;
711 orientations[i] = particle.orientation;
712 }
713 return Result<float>::success(impl_->settings.spacing);
714}
715size_t VolumeFluid::copyPhaseRenderData(VolumeFluidPhase phase, std::vector<glm::vec3>& positions,
716 std::vector<glm::vec4>& colors, size_t maxParticles) const {
717 positions.clear();
718 colors.clear();
719 positions.reserve(std::min(maxParticles, impl_->state.size()));
720 colors.reserve(std::min(maxParticles, impl_->state.size()));
721 size_t selected = 0;
722 for (const auto& particle : impl_->state) {
723 if (particle.material.phase != phase) continue;
724 if (selected < maxParticles) {
725 positions.push_back(particle.position);
726 colors.push_back(particle.color);
727 }
728 ++selected;
729 }
730 return selected;
731}
732void VolumeFluid::clear() {
733 impl_->state.clear();
734 impl_->renderPrevious.clear();
735 impl_->grabberLabels.clear();
736 impl_->grabberLocalPositions.clear();
737 impl_->contacts.clear();
738 impl_->attachments.clear();
739 impl_->stitches.clear();
740 impl_->attachmentLabels.clear();
741 impl_->attachmentOrientationConstraints.clear();
742 impl_->simplexes.clear();
743 impl_->attachmentReactions.clear();
744 impl_->attachmentReactionScratch.clear();
745 impl_->winds.clear();
746 impl_->windScratch.clear();
747 impl_->externalForces.clear();
748 impl_->externalForceScratch.clear();
749 impl_->particleEvents.clear();
750 impl_->droppedParticleEvents = 0;
751}
752
753Result<void> VolumeFluid::killParticle(unsigned particleIndex) {
754 if (particleIndex >= impl_->state.size()) return invalid("Particle index is stale or out of range");
755 impl_->recordParticleEvent(VolumeFluidParticleEventType::Killed, particleIndex, impl_->state[particleIndex]);
756 impl_->state.erase(impl_->state.begin() + particleIndex);
757 impl_->renderPrevious.erase(impl_->renderPrevious.begin() + particleIndex);
758 impl_->grabberLabels.erase(impl_->grabberLabels.begin() + particleIndex);
759 impl_->grabberLocalPositions.erase(impl_->grabberLocalPositions.begin() + particleIndex);
760 std::erase_if(impl_->attachments, [&](auto& attachment) {
761 if (attachment.particleIndex == particleIndex) return true;
762 if (attachment.particleIndex > particleIndex) --attachment.particleIndex;
763 return false;
764 });
765 std::erase_if(impl_->stitches, [&](auto& stitch) {
766 if (stitch.particleIndex1 == particleIndex || stitch.particleIndex2 == particleIndex) return true;
767 if (stitch.particleIndex1 > particleIndex) --stitch.particleIndex1;
768 if (stitch.particleIndex2 > particleIndex) --stitch.particleIndex2;
769 return false;
770 });
771 impl_->attachmentLabels.clear();
772 impl_->attachmentOrientationConstraints.clear();
773 std::erase_if(impl_->simplexes, [&](auto& simplex) {
774 for (unsigned i = 0; i < simplex.size; ++i)
775 if (simplex.particleIndices[i] == particleIndex) return true;
776 for (unsigned i = 0; i < simplex.size; ++i)
777 if (simplex.particleIndices[i] > particleIndex) --simplex.particleIndices[i];
778 return false;
779 });
780 if (!impl_->winds.empty()) impl_->winds.erase(impl_->winds.begin() + particleIndex);
781 if (!impl_->externalForces.empty()) impl_->externalForces.erase(impl_->externalForces.begin() + particleIndex);
782 impl_->contacts.clear();
783 impl_->attachmentReactions.clear();
784 impl_->attachmentReactionScratch.clear();
785 return Result<void>::success();
786}
787
788Result<unsigned> VolumeFluid::killActorParticles(unsigned actorGroup) {
789 if (actorGroup > 0x00ffffffu)
791 Diagnostic::error(DiagnosticCode::InvalidArgument, "Invalid actor group", "fluids.volume.killActor"));
792 const size_t originalSize = impl_->state.size();
793 const size_t removed = size_t(std::count_if(impl_->state.begin(), impl_->state.end(), [&](const auto& particle) {
794 return particle.actorGroup == actorGroup;
795 }));
796 if (removed == 0)
797 return Result<unsigned>::failure(Diagnostic::error(
798 DiagnosticCode::NotFound, "Actor group has no live particles", "fluids.volume.killActor"));
799 impl_->next.resize(originalSize);
800 size_t remaining = 0;
801 for (size_t i = 0; i < originalSize; ++i) {
802 const bool erase = impl_->state[i].actorGroup == actorGroup;
803 impl_->next[i] = erase ? -1 : int(remaining);
804 if (erase) {
805 impl_->recordParticleEvent(VolumeFluidParticleEventType::Killed, unsigned(i), impl_->state[i]);
806 continue;
807 }
808 if (remaining != i) {
809 impl_->state[remaining] = impl_->state[i];
810 impl_->renderPrevious[remaining] = impl_->renderPrevious[i];
811 impl_->grabberLabels[remaining] = impl_->grabberLabels[i];
812 impl_->grabberLocalPositions[remaining] = impl_->grabberLocalPositions[i];
813 if (!impl_->winds.empty()) impl_->winds[remaining] = impl_->winds[i];
814 if (!impl_->externalForces.empty()) impl_->externalForces[remaining] = impl_->externalForces[i];
815 }
816 ++remaining;
817 }
818 impl_->state.resize(remaining);
819 impl_->renderPrevious.resize(remaining);
820 impl_->grabberLabels.resize(remaining);
821 impl_->grabberLocalPositions.resize(remaining);
822 if (!impl_->winds.empty()) impl_->winds.resize(remaining);
823 if (!impl_->externalForces.empty()) impl_->externalForces.resize(remaining);
824 std::erase_if(impl_->attachments, [&](auto& attachment) {
825 const int mapped = impl_->next[attachment.particleIndex];
826 if (mapped < 0) return true;
827 attachment.particleIndex = unsigned(mapped);
828 return false;
829 });
830 std::erase_if(impl_->stitches, [&](auto& stitch) {
831 const int first = impl_->next[stitch.particleIndex1], second = impl_->next[stitch.particleIndex2];
832 if (first < 0 || second < 0) return true;
833 stitch.particleIndex1 = unsigned(first);
834 stitch.particleIndex2 = unsigned(second);
835 return false;
836 });
837 std::erase_if(impl_->simplexes, [&](auto& simplex) {
838 for (unsigned i = 0; i < simplex.size; ++i) {
839 const int mapped = impl_->next[simplex.particleIndices[i]];
840 if (mapped < 0) return true;
841 simplex.particleIndices[i] = unsigned(mapped);
842 }
843 return false;
844 });
845 impl_->attachmentLabels.clear();
846 impl_->attachmentOrientationConstraints.clear();
847 impl_->contacts.clear();
848 impl_->attachmentReactions.clear();
849 impl_->attachmentReactionScratch.clear();
850 return Result<unsigned>::success(unsigned(removed));
851}
852
853Result<unsigned> VolumeFluid::actorParticleCount(unsigned actorGroup) const {
854 if (actorGroup > 0x00ffffffu)
855 return Result<unsigned>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Invalid actor group",
856 "fluids.volume.actorParticleCount"));
858 unsigned(std::count_if(impl_->state.begin(), impl_->state.end(),
859 [&](const auto& particle) { return particle.actorGroup == actorGroup; })));
860}
861
862Result<void> VolumeFluid::killActorParticle(unsigned actorGroup, unsigned actorParticleIndex) {
863 if (actorGroup > 0x00ffffffu)
864 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Invalid actor group",
865 "fluids.volume.killActorParticle"));
866 unsigned localIndex = 0;
867 for (size_t denseIndex = 0; denseIndex < impl_->state.size(); ++denseIndex) {
868 if (impl_->state[denseIndex].actorGroup != actorGroup) continue;
869 if (localIndex == actorParticleIndex) return killParticle(unsigned(denseIndex));
870 ++localIndex;
871 }
872 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument,
873 "Actor particle index is stale or out of range",
874 "fluids.volume.killActorParticle"));
875}
876
877} // namespace eve::fluids
LogicalId target
double value
Duration start
eve::EntitySpatialPose pose
const std::string & s
float phase
Definition CaveMesh.cpp:58
float length
Definition CaveMesh.cpp:94
Vec3 radii
Definition CaveMesh.cpp:56
glm::vec4 p[6]
std::string label
EvpackChunkInput input
Definition Evpack.cpp:170
float maximum[3]
float minimum[3]
std::uint32_t capacity
wgpu::PopErrorScopeStatus status
std::array< double, 10 > q
double r
std::vector< float > positions
std::int32_t c
std::int32_t first
std::string local
Range range
std::array< float, 4 > rotation
bool valid
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
float distance
bool finite
World3D * world
std::array< PixelCell, kPixelChunkSize *kPixelChunkSize > cells
bool hit
#define EV_PROFILE_MODULE(module, name)
Profile the enclosing scope, tagged with a module for grouping.
Definition Profile.h:140
std::vector< float > colors
bool found
int removed
double current
std::uint32_t count
double gravity
TerrainThermalSettings settings
float inertia
Definition TreeMesh.cpp:309
uint32_t index
float angularVelocity
float angle
Move-only operation result carrying either a value or Status.
Definition Result.h:155
CPU position-based free-volume fluid with a bounded spatial grid. @ownership Owns all particle state;...
VolumeFluid(const VolumeFluid &)=delete
GLSL compute kernels for the GPU surface-flow solver.
Definition FluidTarget.h:12
VolumeFluidPhase
Constitutive phase; solids are world-fixed or follow their owned collider attachment.
Definition VolumeFluid.h:20
Result< int > invalid(std::string message)
Invalid.
Owning result of one explicit particle lifecycle-event drain.
std::vector< VolumeFluidParticleEvent > events
uint64_t dropped
Events discarded since the preceding drain because the configured queue was full.
Meter-space (+Y up) volume-fluid material and solver policy.
Definition VolumeFluid.h:82
glm::vec4 ambient