载入中...
搜索中...
未找到
VolumeFluidTools.cpp
浏览该文件的文档.
1#include "fluids/VolumeFluidInternal.inc"
2
3namespace eve::fluids {
4Result<void> VolumeFluid::setSimplexes(std::span<const VolumeFluidSimplex> simplexes) {
5 if (simplexes.size() > 65536) return invalid("Too many fluid simplexes");
6 for (const auto& simplex : simplexes) {
7 if (simplex.size < 1 || simplex.size > 3) return invalid("Simplex size must be 1, 2 or 3");
8 for (unsigned i = 0; i < simplex.size; ++i) {
9 if (simplex.particleIndices[i] >= impl_->state.size())
10 return invalid("Simplex particle index is out of range");
11 for (unsigned j = 0; j < i; ++j)
12 if (simplex.particleIndices[i] == simplex.particleIndices[j])
13 return invalid("Simplex particle indices must be distinct");
14 }
15 }
16 impl_->simplexes.assign(simplexes.begin(), simplexes.end());
17 return Result<void>::success();
18}
19
21 if (!finite(center) || !range(radius, 0.f, 10000.f) || !validMaterial(material))
22 return invalid("Invalid material brush");
23 for (auto& p : impl_->state)
24 if (glm::length(p.position - center) <= radius) {
25 p.material = material;
26 if (material.phase == VolumeFluidPhase::Solid) {
27 p.velocity = glm::vec3(0.f);
28 p.angularVelocity = glm::vec3(0.f);
29 }
30 }
31 std::erase_if(impl_->attachments, [&](const auto& a) {
32 return impl_->state[a.particleIndex].material.phase != VolumeFluidPhase::Solid;
33 });
34 return Result<void>::success();
35}
36
37Result<void> VolumeFluid::applyMaterialChannels(std::span<const VolumeFluidViscosityColorKey> colorKeys) {
38 if (!colorKeys.empty()) {
39 if (colorKeys.size() < 2 || colorKeys.size() > 32) return invalid("Viscosity gradient requires 2..32 keys");
40 float previous = -1.f;
41 for (const auto& key : colorKeys) {
42 if (!range(key.viscosity, 0.f, 100.f) || key.viscosity <= previous || !range(key.color.x, 0.f, 1.f) ||
43 !range(key.color.y, 0.f, 1.f) || !range(key.color.z, 0.f, 1.f) || !range(key.color.w, 0.f, 1.f))
44 return invalid("Invalid viscosity gradient coordinate or color");
45 previous = key.viscosity;
46 }
47 }
48 for (const auto& particle : impl_->state)
49 if (!range(particle.data.x, 0.f, 100.f) || !range(particle.data.y, 0.f, 10.f))
50 return invalid("Material channels exceed viscosity/cohesion ranges");
51 for (auto& particle : impl_->state) {
52 if (!colorKeys.empty()) {
53 const float viscosity = particle.material.viscosity;
54 const auto upper = std::upper_bound(colorKeys.begin(), colorKeys.end(), viscosity,
55 [](float value, const auto& key) { return value < key.viscosity; });
56 if (upper == colorKeys.begin())
57 particle.color = colorKeys.front().color;
58 else if (upper == colorKeys.end())
59 particle.color = colorKeys.back().color;
60 else {
61 const auto& lower = *(upper - 1);
62 const float t = (viscosity - lower.viscosity) / (upper->viscosity - lower.viscosity);
63 particle.color = lower.color * (1.f - t) + upper->color * t;
64 }
65 }
66 particle.material.viscosity = particle.data.x;
67 particle.material.cohesion = particle.data.y;
68 }
69 return Result<void>::success();
70}
71
73 if (!range(color.x, 0.f, 1.f) || !range(color.y, 0.f, 1.f) || !range(color.z, 0.f, 1.f) ||
74 !range(color.w, 0.f, 1.f))
75 return invalid("Invalid solid color");
76 for (auto& particle : impl_->state)
77 if (particle.material.phase == VolumeFluidPhase::Solid) particle.color = color;
78 return Result<void>::success();
79}
80
82 if (!range(sensibility, 1e-6f, 1000.f)) return invalid("Invalid velocity color sensitivity");
83 for (auto& particle : impl_->state) {
84 const auto rgb = glm::clamp(particle.velocity / sensibility, glm::vec3(-1.f), glm::vec3(1.f)) * .5f + .5f;
85 particle.color = {rgb, 1.f};
86 }
87 return Result<void>::success();
88}
89
91 static const std::array<glm::vec4, 26> alphabet = {{{240 / 255.f, 163 / 255.f, 1, 1},
92 {0, 117 / 255.f, 220 / 255.f, 1},
93 {153 / 255.f, 63 / 255.f, 0, 1},
94 {76 / 255.f, 0, 92 / 255.f, 1},
95 {25 / 255.f, 25 / 255.f, 25 / 255.f, 1},
96 {0, 92 / 255.f, 49 / 255.f, 1},
97 {43 / 255.f, 206 / 255.f, 72 / 255.f, 1},
98 {1, 204 / 255.f, 153 / 255.f, 1},
99 {128 / 255.f, 128 / 255.f, 128 / 255.f, 1},
100 {148 / 255.f, 1, 181 / 255.f, 1},
101 {143 / 255.f, 124 / 255.f, 0, 1},
102 {157 / 255.f, 204 / 255.f, 0, 1},
103 {194 / 255.f, 0, 136 / 255.f, 1},
104 {0, 51 / 255.f, 128 / 255.f, 1},
105 {1, 164 / 255.f, 5 / 255.f, 1},
106 {1, 168 / 255.f, 187 / 255.f, 1},
107 {66 / 255.f, 102 / 255.f, 0, 1},
108 {1, 0, 16 / 255.f, 1},
109 {94 / 255.f, 241 / 255.f, 242 / 255.f, 1},
110 {0, 153 / 255.f, 143 / 255.f, 1},
111 {224 / 255.f, 1, 102 / 255.f, 1},
112 {116 / 255.f, 10 / 255.f, 1, 1},
113 {153 / 255.f, 0, 0, 1},
114 {1, 1, 128 / 255.f, 1},
115 {1, 1, 0, 1},
116 {1, 80 / 255.f, 5 / 255.f, 1}}};
117 for (auto& particle : impl_->state) particle.color = alphabet[particle.actorGroup % alphabet.size()];
118 return Result<void>::success();
119}
120
121namespace {
122bool validColorGradient(std::span<const VolumeFluidColorKey> gradient) {
123 if (gradient.size() < 2 || gradient.size() > 32) return false;
124 float previous = -1.f;
125 for (const auto& key : gradient) {
126 if (!range(key.coordinate, 0.f, 1.f) || key.coordinate <= previous || !range(key.color.x, 0.f, 1.f) ||
127 !range(key.color.y, 0.f, 1.f) || !range(key.color.z, 0.f, 1.f) || !range(key.color.w, 0.f, 1.f))
128 return false;
129 previous = key.coordinate;
130 }
131 return true;
132}
133glm::vec4 sampleColorGradient(std::span<const VolumeFluidColorKey> gradient, float value) {
134 const auto upper = std::upper_bound(gradient.begin(), gradient.end(), value,
135 [](float coordinate, const auto& key) { return coordinate < key.coordinate; });
136 if (upper == gradient.begin()) return gradient.front().color;
137 if (upper == gradient.end()) return gradient.back().color;
138 const auto& lower = *(upper - 1);
139 const float t = (value - lower.coordinate) / (upper->coordinate - lower.coordinate);
140 return lower.color * (1.f - t) + upper->color * t;
141}
142float colorRandom(uint32_t& state) {
143 state += 0x9e3779b9u;
144 uint32_t x = state;
145 x = (x ^ (x >> 16u)) * 0x85ebca6bu;
146 x = (x ^ (x >> 13u)) * 0xc2b2ae35u;
147 x ^= x >> 16u;
148 return float(x >> 8u) * (1.f / 16777216.f);
149}
150} // namespace
151
152Result<void> VolumeFluid::applyDataColors(unsigned channel, std::span<const VolumeFluidColorKey> gradient) {
153 if (channel > 3 || !validColorGradient(gradient)) return invalid("Invalid data color channel or gradient");
154 for (auto& particle : impl_->state) particle.color = sampleColorGradient(gradient, particle.data[channel]);
155 return Result<void>::success();
156}
157
158Result<void> VolumeFluid::applyRandomColors(std::span<const VolumeFluidColorKey> gradient, uint32_t seed) {
159 if (!validColorGradient(gradient)) return invalid("Invalid random color gradient");
160 for (auto& particle : impl_->state) particle.color = sampleColorGradient(gradient, colorRandom(seed));
161 return Result<void>::success();
162}
163
165 if (!range(intensity, 0.f, 100.f)) return invalid("Invalid random velocity intensity");
166 const float z = colorRandom(seed) * 2.f - 1.f;
167 const float phi = colorRandom(seed) * glm::two_pi<float>();
168 const float radial = std::sqrt(std::max(0.f, 1.f - z * z));
169 const glm::vec3 impulse(radial * std::cos(phi), radial * std::sin(phi), z);
170 for (const auto& particle : impl_->state)
171 if (glm::length(particle.velocity + impulse * intensity) > 100.f)
172 return invalid("Random velocity impulse exceeds particle speed limit");
173 for (auto& particle : impl_->state) particle.velocity += impulse * intensity;
174 return Result<void>::success();
175}
176
177Result<void> VolumeFluid::teleportActor(glm::vec3 currentPosition, glm::vec4 currentRotation, glm::vec3 targetPosition,
178 glm::vec4 targetRotation) {
179 const auto validPose = [](glm::vec3 position, glm::vec4 rotation) {
180 return finite(position) && finite(rotation) && std::abs(glm::dot(rotation, rotation) - 1.f) <= .001f;
181 };
182 if (!validPose(currentPosition, currentRotation) || !validPose(targetPosition, targetRotation))
183 return invalid("Invalid actor teleport pose");
184 const glm::quat from(currentRotation.w, currentRotation.x, currentRotation.y, currentRotation.z);
185 const glm::quat to(targetRotation.w, targetRotation.x, targetRotation.y, targetRotation.z);
186 const auto rotation = glm::normalize(to) * glm::conjugate(glm::normalize(from));
187 for (const auto& particle : impl_->state) {
188 const auto position = targetPosition + rotation * (particle.position - currentPosition);
189 if (!finite(position) || glm::any(glm::notEqual(impl_->constrain(position), position)))
190 return invalid("Actor teleport moves a particle outside solver bounds");
191 }
192 for (auto& particle : impl_->state) {
193 particle.position = targetPosition + rotation * (particle.position - currentPosition);
194 const glm::quat orientation(particle.orientation.w, particle.orientation.x, particle.orientation.y,
195 particle.orientation.z);
196 const auto transformed = glm::normalize(rotation * orientation);
197 particle.orientation = {transformed.x, transformed.y, transformed.z, transformed.w};
198 particle.velocity = glm::vec3(0.f);
199 particle.angularVelocity = glm::vec3(0.f);
200 }
201 impl_->renderPrevious.resize(impl_->state.size());
202 for (size_t i = 0; i < impl_->state.size(); ++i) impl_->renderPrevious[i] = impl_->state[i].position;
203 impl_->contacts.clear();
204 impl_->attachmentReactions.clear();
205 impl_->attachmentReactionScratch.clear();
206 std::fill(impl_->grabberLabels.begin(), impl_->grabberLabels.end(), Impl::noGrabber);
207 return Result<void>::success();
208}
209
211 if (actorGroup > 0x00ffffffu)
213 DiagnosticCode::InvalidArgument, "Actor group exceeds 24-bit range", "fluids.volume.actorMass"));
214 double mass = 0.0;
215 glm::dvec3 weightedPosition(0.0);
216 unsigned count = 0;
217 for (const auto& particle : impl_->state) {
218 if (particle.actorGroup != actorGroup) continue;
219 const double particleMass = double(impl_->volume) * double(particle.material.density);
220 mass += particleMass;
221 weightedPosition += glm::dvec3(particle.position) * particleMass;
222 ++count;
223 }
224 if (count == 0)
226 DiagnosticCode::NotFound, "Actor group has no live particles", "fluids.volume.actorMass"));
228 properties.mass = float(mass);
229 properties.centerOfMass = glm::vec3(weightedPosition / mass);
230 properties.particleCount = count;
232}
233
235 if (actorGroup > 0x00ffffffu || category > 15u) return invalid("Invalid actor group or filter category");
236 if (std::none_of(impl_->state.begin(), impl_->state.end(),
237 [&](const auto& particle) { return particle.actorGroup == actorGroup; }))
239 Diagnostic::error(DiagnosticCode::NotFound, "Actor group has no live particles", "fluids.volume.filter"));
240 const unsigned categoryBit = 1u << category;
241 for (auto& particle : impl_->state)
242 if (particle.actorGroup == actorGroup)
243 particle.collisionFilter = (particle.collisionFilter & 0xffff0000u) | categoryBit;
244 return Result<void>::success();
245}
246
247Result<void> VolumeFluid::setActorCollisionFilter(unsigned actorGroup, unsigned collisionFilter) {
248 if (actorGroup > 0x00ffffffu || !validFilter(collisionFilter))
249 return invalid("Invalid actor group or collision filter");
250 if (std::none_of(impl_->state.begin(), impl_->state.end(),
251 [&](const auto& particle) { return particle.actorGroup == actorGroup; }))
253 Diagnostic::error(DiagnosticCode::NotFound, "Actor group has no live particles", "fluids.volume.filter"));
254 for (auto& particle : impl_->state)
255 if (particle.actorGroup == actorGroup) particle.collisionFilter = collisionFilter;
256 return Result<void>::success();
257}
258
260 const bool automaticRadii = glm::all(glm::equal(prototype.radii, glm::vec3(0.f)));
261 const bool explicitRadii = finite(prototype.radii) &&
262 glm::all(glm::greaterThanEqual(prototype.radii, glm::vec3(.0005f))) &&
263 glm::all(glm::lessThanEqual(prototype.radii, glm::vec3(10.f)));
264 if (actorGroup > 0x00ffffffu || !validMaterial(prototype.material) || !finite(prototype.data) ||
265 (!automaticRadii && !explicitRadii) || !validFilter(prototype.collisionFilter))
266 return invalid("Invalid actor group or emitter material prototype");
267 if (std::none_of(impl_->state.begin(), impl_->state.end(),
268 [&](const auto& particle) { return particle.actorGroup == actorGroup; }))
269 return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "Actor group has no live particles",
270 "fluids.volume.actorMaterial"));
271 const auto radii = glm::all(glm::equal(prototype.radii, glm::vec3(0.f))) ? glm::vec3(impl_->settings.spacing * .5f)
272 : prototype.radii;
273 for (auto& particle : impl_->state) {
274 if (particle.actorGroup != actorGroup) continue;
275 particle.material = prototype.material;
276 particle.data = prototype.data;
277 particle.radii = radii;
278 particle.collisionFilter = prototype.collisionFilter;
279 particle.selfCollide = prototype.selfCollide;
280 }
281 return Result<void>::success();
282}
283
285 if (actorGroup > 0x00ffffffu) return invalid("Invalid actor group");
286 if (std::none_of(impl_->state.begin(), impl_->state.end(),
287 [&](const auto& particle) { return particle.actorGroup == actorGroup; }))
288 return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "Actor group has no live particles",
289 "fluids.volume.selfCollisions"));
290 for (auto& particle : impl_->state)
291 if (particle.actorGroup == actorGroup) particle.selfCollide = enabled;
292 return Result<void>::success();
293}
294
295Result<void> VolumeFluid::applyParticleDrag(unsigned particleIndex, glm::vec3 targetPosition, float stiffness,
296 float damping, float seconds) {
297 if (particleIndex >= impl_->state.size() || !finite(targetPosition) || !range(stiffness, 0.f, 10000.f) ||
298 !range(damping, 0.f, 10000.f) || !range(seconds, std::numeric_limits<float>::min(), 1.f / 30.f))
299 return invalid("Invalid particle drag input");
300 auto& particle = impl_->state[particleIndex];
301 const auto acceleration = (targetPosition - particle.position) * stiffness - particle.velocity * damping;
302 const auto velocity = particle.velocity + acceleration * seconds;
303 if (!finite(velocity) || glm::length(velocity) > 1000.f) return invalid("Particle drag exceeds speed limit");
304 particle.velocity = velocity;
305 return Result<void>::success();
306}
307
308Result<unsigned> VolumeFluid::grabContactParticles(unsigned colliderLabel, glm::vec3 position, glm::vec4 rotation,
309 float distanceThreshold) {
310 const auto failure = [](const char* message) {
312 Diagnostic::error(DiagnosticCode::InvalidArgument, message, "fluids.volume.contactGrabber"));
313 };
314 if (!finite(position) || !finite(rotation) || std::abs(glm::dot(rotation, rotation) - 1.f) > .001f ||
315 !range(distanceThreshold, 0.f, .1f))
316 return failure("Invalid contact-grabber pose or distance threshold");
317 if (!impl_->hasColliderLabel(colliderLabel)) return failure("Unknown contact-grabber collider label");
318
319 impl_->grabberSelection.assign(impl_->state.size(), uint8_t(0));
320 for (const auto& contact : impl_->contacts) {
321 if (contact.colliderLabel != colliderLabel || contact.distance >= distanceThreshold ||
322 contact.particleIndex >= impl_->state.size())
323 continue;
324 if (impl_->state[contact.particleIndex].material.phase != VolumeFluidPhase::Solid)
325 impl_->grabberSelection[contact.particleIndex] = 1;
326 }
327 for (size_t i = 0; i < impl_->state.size(); ++i)
328 if (impl_->grabberSelection[i] && impl_->grabberLabels[i] != Impl::noGrabber &&
329 impl_->grabberLabels[i] != int64_t(colliderLabel))
330 return failure("Particle is already owned by another contact grabber");
331
332 const auto q = glm::normalize(glm::quat(rotation.w, rotation.x, rotation.y, rotation.z));
333 const auto inverseRotation = glm::transpose(glm::mat3_cast(q));
334 impl_->grabberLabelScratch = impl_->grabberLabels;
335 impl_->grabberLocalScratch = impl_->grabberLocalPositions;
336 unsigned count = 0;
337 for (size_t i = 0; i < impl_->state.size(); ++i) {
338 if (impl_->grabberLabelScratch[i] == int64_t(colliderLabel)) impl_->grabberLabelScratch[i] = Impl::noGrabber;
339 if (!impl_->grabberSelection[i]) continue;
340 const auto local = inverseRotation * (impl_->state[i].position - position);
341 if (!finite(local)) return failure("Nonfinite contact-grabber local position");
342 impl_->grabberLabelScratch[i] = int64_t(colliderLabel);
343 impl_->grabberLocalScratch[i] = local;
344 ++count;
345 }
346 impl_->grabberLabels.swap(impl_->grabberLabelScratch);
347 impl_->grabberLocalPositions.swap(impl_->grabberLocalScratch);
348 for (size_t i = 0; i < impl_->state.size(); ++i)
349 if (impl_->grabberLabels[i] == int64_t(colliderLabel)) {
350 impl_->state[i].velocity = glm::vec3(0.f);
351 impl_->state[i].angularVelocity = glm::vec3(0.f);
352 }
354}
355
356Result<unsigned> VolumeFluid::updateGrabbedParticles(unsigned colliderLabel, glm::vec3 position, glm::vec4 rotation) {
357 const auto failure = [](const char* message) {
359 Diagnostic::error(DiagnosticCode::InvalidArgument, message, "fluids.volume.contactGrabber"));
360 };
361 if (!finite(position) || !finite(rotation) || std::abs(glm::dot(rotation, rotation) - 1.f) > .001f)
362 return failure("Invalid contact-grabber pose");
363 const auto q = glm::normalize(glm::quat(rotation.w, rotation.x, rotation.y, rotation.z));
364 const auto transform = glm::mat3_cast(q);
365 unsigned count = 0;
366 for (size_t i = 0; i < impl_->state.size(); ++i) {
367 if (impl_->grabberLabels[i] != int64_t(colliderLabel)) continue;
368 const auto target = position + transform * impl_->grabberLocalPositions[i];
369 if (!finite(target) || glm::any(glm::notEqual(impl_->constrain(target), target)))
370 return failure("Contact-grabber target is outside solver bounds");
371 ++count;
372 }
373 for (size_t i = 0; i < impl_->state.size(); ++i) {
374 if (impl_->grabberLabels[i] != int64_t(colliderLabel)) continue;
375 const auto target = position + transform * impl_->grabberLocalPositions[i];
376 impl_->state[i].position = target;
377 impl_->state[i].velocity = glm::vec3(0.f);
378 impl_->state[i].angularVelocity = glm::vec3(0.f);
379 impl_->renderPrevious[i] = target;
380 }
381 impl_->contacts.clear();
383}
384
386 unsigned count = 0;
387 for (auto& label : impl_->grabberLabels)
388 if (label == int64_t(colliderLabel)) {
389 label = Impl::noGrabber;
390 ++count;
391 }
393}
394
396 std::vector<VolumeFluidFieldSample> samples;
397 auto sampled = sampleFieldInto(positions, samples);
398 if (!sampled) return Result<std::vector<VolumeFluidFieldSample>>::failure(sampled.status());
399 return Result<std::vector<VolumeFluidFieldSample>>::success(std::move(samples));
400}
401
402Result<void> VolumeFluid::sampleFieldInto(std::span<const glm::vec3> positions,
403 std::vector<VolumeFluidFieldSample>& samples) const {
404 const auto failure = [](const char* message) {
407 };
408 if (positions.size() > 65536 || std::any_of(positions.begin(), positions.end(), [](auto p) { return !finite(p); }))
409 return failure("Invalid field query positions or count");
410 samples.assign(positions.size(), VolumeFluidFieldSample{});
411 if (positions.empty() || impl_->state.empty()) return Result<void>::success();
412 impl_->grid();
413 size_t visits = 0;
414 const float h2 = impl_->h * impl_->h;
415 for (size_t i = 0; i < positions.size(); ++i) {
416 const auto position = positions[i];
417 if (glm::any(glm::lessThan(position, impl_->settings.minimum - impl_->h)) ||
418 glm::any(glm::greaterThan(position, impl_->settings.maximum + impl_->h)))
419 continue;
420 const auto cell = impl_->cell(position);
421 const auto low = glm::max(cell - 1, glm::ivec3(0)), high = glm::min(cell + 1, impl_->dims - 1);
422 auto& sample = samples[i];
423 float weightSum = 0.f;
424 glm::vec3 gradientSum(0.f), curlSum(0.f), referenceVelocity(0.f);
425 for (int z = low.z; z <= high.z; ++z)
426 for (int y = low.y; y <= high.y; ++y)
427 for (int x = low.x; x <= high.x; ++x) {
428 for (int j = impl_->heads[impl_->index({x, y, z})]; j >= 0; j = impl_->next[size_t(j)]) {
429 if (++visits > 4000000) return failure("Field query candidate budget exceeded");
430 const auto& particle = impl_->state[size_t(j)];
431 if (!fluid(particle.material)) continue;
432 const auto dx = position - particle.position;
433 const float r2 = glm::dot(dx, dx);
434 if (r2 >= h2) continue;
435 const float q = 1.f - r2 / h2;
436 const float weight = impl_->volume * impl_->kernel(r2);
437 const auto gradient = dx * (-6.f * impl_->volume * impl_->selfKernel * q * q / h2);
438 if (sample.neighborCount == 0) referenceVelocity = particle.velocity;
439 ++sample.neighborCount;
440 const auto relativeVelocity = particle.velocity - referenceVelocity;
441 sample.density += particle.material.density * weight;
442 sample.velocity += relativeVelocity * weight;
443 gradientSum += gradient;
444 curlSum += glm::cross(gradient, relativeVelocity);
445 weightSum += weight;
446 }
447 }
448 if (weightSum > 1e-12f) {
449 sample.velocity /= weightSum;
450 sample.vorticity = (curlSum - glm::cross(gradientSum, sample.velocity)) / weightSum;
451 sample.velocity += referenceVelocity;
452 } else {
453 sample.velocity = sample.vorticity = glm::vec3(0.f);
454 }
455 }
456 return Result<void>::success();
457}
458
460 if (maxCells == 0 || maxCells > 1000000)
462 DiagnosticCode::InvalidArgument, "Invalid particle-grid debug cell budget", "fluids.volume.gridDebug"));
463 auto& indices = impl_->debugCellIndices;
464 indices.resize(impl_->state.size());
465 for (size_t i = 0; i < impl_->state.size(); ++i) indices[i] = impl_->index(impl_->cell(impl_->state[i].position));
466 std::sort(indices.begin(), indices.end());
467
468 size_t occupied = 0;
469 for (size_t i = 0; i < indices.size();) {
470 const size_t cell = indices[i];
471 while (i < indices.size() && indices[i] == cell) ++i;
472 ++occupied;
473 }
474 if (occupied > maxCells)
476 DiagnosticCode::InvalidArgument, "Particle-grid debug cell budget exceeded", "fluids.volume.gridDebug"));
477
478 std::vector<VolumeFluidGridCell> cells;
479 cells.reserve(occupied);
480 for (size_t i = 0; i < indices.size();) {
481 const size_t linear = indices[i];
482 size_t end = i + 1;
483 while (end < indices.size() && indices[end] == linear) ++end;
484 const int x = int(linear % size_t(impl_->dims.x));
485 const size_t yz = linear / size_t(impl_->dims.x);
486 const int y = int(yz % size_t(impl_->dims.y));
487 const int z = int(yz / size_t(impl_->dims.y));
488 const auto center = impl_->settings.minimum + (glm::vec3(x, y, z) + .5f) * impl_->h;
489 cells.push_back({center, glm::vec3(impl_->h), unsigned(end - i)});
490 i = end;
491 }
492 return Result<std::vector<VolumeFluidGridCell>>::success(std::move(cells));
493}
494
496 unsigned maxParticles) const {
497 const auto failure = [](DiagnosticCode code, const char* message) {
499 Diagnostic::error(code, message, "fluids.volume.frameDebug"));
500 };
501 if (actorGroup > 0x00ffffffu || !range(size, std::numeric_limits<float>::min(), 100.f) || maxParticles == 0 ||
502 maxParticles > 1000000)
503 return failure(DiagnosticCode::InvalidArgument, "Invalid particle-frame debug input");
504 const size_t count = size_t(std::count_if(impl_->state.begin(), impl_->state.end(),
505 [&](const auto& p) { return p.actorGroup == actorGroup; }));
506 if (count == 0) return failure(DiagnosticCode::NotFound, "Actor group has no live particles");
507 if (count > maxParticles) return failure(DiagnosticCode::InvalidArgument, "Particle-frame debug budget exceeded");
508 std::vector<VolumeFluidParticleFrame> frames;
509 frames.reserve(count);
510 for (size_t i = 0; i < impl_->state.size(); ++i) {
511 const auto& particle = impl_->state[i];
512 if (particle.actorGroup != actorGroup) continue;
513 const auto rotation = glm::mat3_cast(particleQuaternion(particle));
514 frames.push_back({unsigned(i), particle.position, particle.position + rotation[0] * size,
515 particle.position + rotation[1] * size, particle.position + rotation[2] * size});
516 }
517 return Result<std::vector<VolumeFluidParticleFrame>>::success(std::move(frames));
518}
519
521 glm::vec3 instanceScale, float alpha,
522 unsigned maxInstances) const {
523 const auto failure = [](DiagnosticCode code, const char* message) {
525 Diagnostic::error(code, message, "fluids.volume.particleInstances"));
526 };
527 if (actorGroup > 0x00ffffffu || !finite(instanceScale) ||
528 glm::any(glm::lessThanEqual(instanceScale, glm::vec3(0.f))) ||
529 glm::any(glm::greaterThan(instanceScale, glm::vec3(10000.f))) || !range(alpha, 0.f, 1.f) || maxInstances == 0 ||
530 maxInstances > 1000000)
531 return failure(DiagnosticCode::InvalidArgument, "Invalid particle-instance input");
532 const size_t count = size_t(std::count_if(impl_->state.begin(), impl_->state.end(),
533 [&](const auto& particle) { return particle.actorGroup == actorGroup; }));
534 if (count == 0) return failure(DiagnosticCode::NotFound, "Particle-instance actor group was not found");
535 if (count > maxInstances)
536 return failure(DiagnosticCode::InvalidArgument, "Particle-instance output budget exceeded");
537 std::vector<VolumeFluidParticleInstance> instances;
538 instances.reserve(count);
539 for (size_t index = 0; index < impl_->state.size(); ++index) {
540 const auto& particle = impl_->state[index];
541 if (particle.actorGroup != actorGroup) continue;
542 instances.push_back({unsigned(index), interpolatedPositionUnchecked(index, alpha), particle.orientation,
543 particle.radii * instanceScale, particle.color});
544 }
546}
547
549 glm::vec4 tint, float alpha,
550 unsigned maxInstances) const {
551 std::vector<VolumeFluidParticleInstance> instances;
552 auto copied = copyParticleImpostors(actorGroup, radiusScale, tint, alpha, maxInstances, instances);
553 if (!copied) return Result<std::vector<VolumeFluidParticleInstance>>::failure(copied.status());
555}
556
557Result<void> VolumeFluid::copyParticleImpostors(unsigned actorGroup, float radiusScale, glm::vec4 tint, float alpha,
558 unsigned maxInstances,
559 std::vector<VolumeFluidParticleInstance>& instances) const {
560 const auto failure = [](DiagnosticCode code, const char* message) {
561 return Result<void>::failure(Diagnostic::error(code, message, "fluids.volume.particleImpostors"));
562 };
563 if (actorGroup > 0x00ffffffu || !range(radiusScale, std::numeric_limits<float>::min(), 10000.f) || !finite(tint) ||
564 glm::any(glm::lessThan(tint, glm::vec4(0.f))) || glm::any(glm::greaterThan(tint, glm::vec4(1.f))) ||
565 !range(alpha, 0.f, 1.f) || maxInstances == 0 || maxInstances > 1000000)
566 return failure(DiagnosticCode::InvalidArgument, "Invalid particle-impostor input");
567 const size_t count = size_t(std::count_if(impl_->state.begin(), impl_->state.end(),
568 [&](const auto& particle) { return particle.actorGroup == actorGroup; }));
569 if (count == 0) return failure(DiagnosticCode::NotFound, "Particle-impostor actor group was not found");
570 if (count > maxInstances)
571 return failure(DiagnosticCode::InvalidArgument, "Particle-impostor output budget exceeded");
572 instances.resize(count);
573 size_t outputIndex = 0;
574 for (size_t index = 0; index < impl_->state.size(); ++index) {
575 const auto& particle = impl_->state[index];
576 if (particle.actorGroup != actorGroup) continue;
577 instances[outputIndex++] = {unsigned(index), interpolatedPositionUnchecked(index, alpha), particle.orientation,
578 particle.radii * radiusScale, particle.color * tint};
579 }
580 return Result<void>::success();
581}
582
584 float slice, float maxDistance, unsigned maxSamples) const {
585 const auto failure = [](DiagnosticCode code, const char* message) {
586 return Result<VolumeFluidSdfSlice>::failure(Diagnostic::error(code, message, "fluids.volume.sdfSliceDebug"));
587 };
588 const int axisIndex = int(axis);
589 if (axisIndex < 0 || axisIndex > 2 || !range(slice, 0.f, 1.f) ||
590 !range(maxDistance, std::numeric_limits<float>::min(), 10000.f) || maxSamples == 0 || maxSamples > 1000000)
591 return failure(DiagnosticCode::InvalidArgument, "Invalid SDF slice debug input");
592 const auto found = std::find_if(impl_->sdfColliders.begin(), impl_->sdfColliders.end(),
593 [&](const auto& collider) { return collider.label == colliderLabel; });
594 if (found == impl_->sdfColliders.end())
595 return failure(DiagnosticCode::NotFound, "SDF collider label was not found");
596
597 const auto& collider = *found;
598 const auto& sdf = collider.sdf;
599 const int uAxis = axis == VolumeFluidSdfSliceAxis::X ? 2 : 0;
600 const int vAxis = axis == VolumeFluidSdfSliceAxis::Y ? 2 : 1;
601 const auto width = unsigned(sdf.dims[uAxis]);
602 const auto height = unsigned(sdf.dims[vAxis]);
603 if (uint64_t(width) * uint64_t(height) > maxSamples)
604 return failure(DiagnosticCode::InvalidArgument, "SDF slice sample budget exceeded");
605
606 glm::vec3 localOrigin = sdf.origin;
607 localOrigin[axisIndex] += float(sdf.dims[axisIndex] - 1) * sdf.cellSize * slice;
608 glm::vec3 localStepX(0.f), localStepY(0.f);
609 localStepX[uAxis] = sdf.cellSize;
610 localStepY[vAxis] = sdf.cellSize;
611 const auto rotation = impl_->sdfColliderRotations[size_t(found - impl_->sdfColliders.begin())];
612
615 output.height = height;
616 output.origin = collider.position + rotation * (localOrigin * collider.scale);
617 output.stepX = rotation * (localStepX * collider.scale);
618 output.stepY = rotation * (localStepY * collider.scale);
619 output.values.resize(size_t(width) * size_t(height));
620 for (unsigned y = 0; y < height; ++y)
621 for (unsigned x = 0; x < width; ++x) {
622 const glm::vec3 local = localOrigin + localStepX * float(x) + localStepY * float(y);
623 const float normalized = .5f + .5f * sdf.sample(local) / maxDistance;
624 output.values[size_t(y) * width + x] = std::clamp(normalized, 0.f, 1.f);
625 }
627}
628
630 return {"eve.volume-fluid",
631 20,
632 impl_->settings,
633 impl_->state,
634 impl_->colliders,
635 impl_->sdfColliders,
636 impl_->heightFieldColliders,
637 impl_->attachments,
638 impl_->stitches,
639 impl_->simplexes};
640}
641
643 if (snapshot.schema != "eve.volume-fluid" || snapshot.version != 20)
644 return invalid("Unsupported volume-fluid schema/version");
645 auto candidate = create(snapshot.settings);
646 if (!candidate) return Result<void>::failure(candidate.status());
647 auto replacement = std::move(candidate).takeValue();
648 auto particles = replacement->emit(snapshot.particles);
649 if (!particles) return particles;
650 auto colliders = replacement->setColliders(snapshot.colliders);
651 if (!colliders) return colliders;
652 auto sdfColliders = replacement->setSdfColliders(snapshot.sdfColliders);
653 if (!sdfColliders) return sdfColliders;
654 auto heightFields = replacement->setHeightFieldColliders(snapshot.heightFieldColliders);
655 if (!heightFields) return heightFields;
656 if (snapshot.attachments.size() > snapshot.particles.size()) return invalid("Too many particle attachments");
657 std::vector<bool> attached(snapshot.particles.size(), false);
658 std::vector<unsigned> labels;
659 labels.reserve(snapshot.colliders.size());
660 for (const auto& c : snapshot.colliders) labels.push_back(c.label);
661 std::sort(labels.begin(), labels.end());
662 for (const auto& a : snapshot.attachments) {
663 if (a.particleIndex >= snapshot.particles.size() || attached[a.particleIndex] || !finite(a.localPosition) ||
664 !finite(a.localOrientation) || std::abs(glm::dot(a.localOrientation, a.localOrientation) - 1.f) > .001f ||
665 !range(a.compliance, 0.f, 1000000.f) || !range(a.breakThreshold, 1e-6f, 1e12f) ||
666 !std::binary_search(labels.begin(), labels.end(), a.colliderLabel))
667 return invalid("Invalid particle attachment reference");
668 attached[a.particleIndex] = true;
669 }
670 replacement->impl_->attachments = snapshot.attachments;
671 auto stitches = replacement->setStitches(snapshot.stitches);
672 if (!stitches) return stitches;
673 auto topology = replacement->setSimplexes(snapshot.simplexes);
674 if (!topology) return topology;
675 impl_.swap(replacement->impl_);
676 return Result<void>::success();
677}
678
679} // namespace eve::fluids
LogicalId target
double value
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
std::string output
std::string from
std::vector< BuildingInstanceSnapshot > instances
Vec3 radii
Definition CaveMesh.cpp:56
glm::vec4 p[6]
std::string label
std::string message
DiagnosticCode code
std::uint32_t key
glm::vec4 tint
std::array< double, 10 > q
std::vector< std::uint32_t > indices
std::vector< float > positions
std::int32_t c
HexCoordinates to
Cell the unit walks towards on this segment.
Definition HexUnits.cpp:64
std::uint32_t height
std::uint32_t width
std::string local
Range range
std::array< float, 4 > rotation
std::array< float, 3 > position
std::map< std::string, PropertyRule > properties
MeleePoint3 a
Definition MeleeHit.cpp:40
graphics::Canvas * previous
bool finite
Topology topology
std::array< PixelCell, kPixelChunkSize *kPixelChunkSize > cells
float radius
std::uint32_t seed
Definition PointSet.cpp:807
float t
Material * material
bool found
float dx
bool occupied
std::uint32_t count
Cell cell
float size
Definition TreeMesh.cpp:156
const char * category
uint32_t index
std::vector< double > phi
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
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
Result< void > setSimplexes(std::span< const VolumeFluidSimplex > simplexes)
Atomically replaces point/edge/triangle topology over current particle indices.
static Result< std::unique_ptr< VolumeFluid > > create(const VolumeFluidSettings &settings)
Validates settings before allocating an owning solver; errors publish no state.
Result< void > applyActorGroupColors()
Applies Fluid3D's 26-color alphabet by actorGroup modulo 26.
Result< void > restore(const VolumeFluidSnapshot &snapshot)
Validates a complete candidate before replacing state; failures preserve the current solver.
Result< void > applyRandomColors(std::span< const VolumeFluidColorKey > gradient, uint32_t seed)
Applies a deterministic Fluid3D ColorRandomizer-style gradient sample to every particle.
Result< void > applyParticleDrag(unsigned particleIndex, glm::vec3 targetPosition, float stiffness, float damping, float seconds)
Applies one fixed-step Fluid3DParticleDragger spring response.
Result< std::vector< VolumeFluidParticleInstance > > particleInstances(unsigned actorGroup, glm::vec3 instanceScale=glm::vec3(1.f), float alpha=1.f, unsigned maxInstances=65536) const
Copies one actor group's interpolated mesh instances for batched particle rendering.
Result< std::vector< VolumeFluidGridCell > > debugParticleGrid(unsigned maxCells=65536) const
Returns occupied particle-grid cells for explicit debug visualization.
Result< void > paint(glm::vec3 center, float radius, const VolumeFluidMaterial &material)
Atomically changes the material of particles in a sphere (supports melting/solidifying).
Result< void > copyParticleImpostors(unsigned actorGroup, float radiusScale, glm::vec4 tint, float alpha, unsigned maxInstances, std::vector< VolumeFluidParticleInstance > &instances) const
Writes Fluid3DParticleRenderer-style instances into caller-owned reusable storage.
Result< std::vector< VolumeFluidParticleInstance > > particleImpostors(unsigned actorGroup, float radiusScale, glm::vec4 tint=glm::vec4(1.f), float alpha=1.f, unsigned maxInstances=65536) const
Copies one actor group's Fluid3DParticleRenderer-style anisotropic impostor instances.
Result< void > applyVelocityColors(float sensibility)
Applies Fluid3D ColorFromVelocity RGB mapping to all particles in place.
Result< void > addRandomVelocity(float intensity, uint32_t seed)
Applies one deterministic random velocity impulse to the whole actor.
Result< void > setActorSelfCollisions(unsigned actorGroup, bool enabled)
Applies Fluid3DEmitter::SetSelfCollisions to all live particles in one actor group.
Result< void > setActorCollisionFilter(unsigned actorGroup, unsigned collisionFilter)
Applies Fluid3DEmitter.Filter to every live particle in one actor group.
Result< void > applySolidColor(glm::vec4 color)
Assigns a linear RGBA color to all current solid particles in place.
Result< VolumeFluidSdfSlice > debugSdfSlice(unsigned colliderLabel, VolumeFluidSdfSliceAxis axis, float slice=.25f, float maxDistance=.5f, unsigned maxSamples=65536) const
Samples one owned SDF collider slice for Fluid3DDistanceFieldRenderer-style diagnostics.
Result< std::vector< VolumeFluidParticleFrame > > debugParticleFrames(unsigned actorGroup, float size=1.f, unsigned maxParticles=65536) const
Returns particle-local XYZ rays for one actor group.
Result< unsigned > releaseGrabbedParticles(unsigned colliderLabel)
Releases one grabber label and restores normal particle dynamics.
Result< std::vector< VolumeFluidFieldSample > > sampleField(std::span< const glm::vec3 > positions) const
Samples the current liquid/gas field at up to 65536 finite world positions.
VolumeFluidSnapshot snapshot() const
Returns complete owning state; scratch grids and last-step contacts are derived and omitted.
Result< void > applyMaterialChannels(std::span< const VolumeFluidViscosityColorKey > colorKeys={})
Atomically maps user data x/y to material viscosity/cohesion for all particles.
Result< void > teleportActor(glm::vec3 currentPosition, glm::vec4 currentRotation, glm::vec3 targetPosition, glm::vec4 targetRotation)
Teleports the whole fluid actor between explicit world poses.
Result< void > updateActorMaterial(unsigned actorGroup, const VolumeFluidParticle &prototype)
Applies Fluid3DEmitter::UpdateParticleMaterial to one live actor group.
std::vector< VolumeFluidParticle > particles() const
Returns an owning snapshot, independent of subsequent mutation or destruction.
Result< unsigned > grabContactParticles(unsigned colliderLabel, glm::vec3 position, glm::vec4 rotation, float distanceThreshold=.01f)
Freezes current contacts for one Fluid3DContactGrabber-compatible collider.
Result< void > setActorFilterCategory(unsigned actorGroup, unsigned category)
Applies Fluid3D SetFilterCategory to all particles in one actor group.
Result< VolumeFluidMassProperties > actorMassProperties(unsigned actorGroup) const
Computes mass and world-space center for one actor group.
Result< unsigned > updateGrabbedParticles(unsigned colliderLabel, glm::vec3 position, glm::vec4 rotation)
Moves all particles owned by one contact grabber to a new world pose.
Result< void > applyDataColors(unsigned channel, std::span< const VolumeFluidColorKey > gradient)
Colors particles by one normalized application-data channel.
GLSL compute kernels for the GPU surface-flow solver.
Definition FluidTarget.h:12
VolumeFluidSdfSliceAxis
Local axis normal used by an explicit SDF diagnostic slice.
double sample(const Heightmap &map, double u, double v)
Sample.
DiagnosticCode
Stable machine-readable diagnostic codes.
Definition Diagnostic.h:47
bool enabled
Owning mass summary for one Fluid3D-compatible actor group.
float mass
Sum of particle rest masses in kilograms.
Per-particle material, copied at emission; densities enable multiphase buoyancy.
Definition VolumeFluid.h:26
Owning particle value with world position and velocity in SI units.
Definition VolumeFluid.h:96
VolumeFluidMaterial material
Owned material parameters.
unsigned collisionFilter
Query collision filter: low 16 bits category, high 16 bits mask.
bool selfCollide
Allows interaction with particles in the same actor group.
glm::vec3 radii
Principal collision/query radii in meters; all zero resolves to spacing/2 on emission.
Owning normalized SDF slice plus its world-space sample lattice.
Canonical version-20 owning state; older codecs migrate collider, attachment, stitch,...
std::vector< VolumeFluidCollider > colliders
std::vector< VolumeFluidSdfCollider > sdfColliders
std::vector< VolumeFluidAttachment > attachments
std::vector< VolumeFluidStitch > stitches
std::vector< VolumeFluidSimplex > simplexes
Explicit point/edge/triangle topology; empty means one implicit point simplex per particle.
std::vector< VolumeFluidParticle > particles
std::vector< VolumeFluidHeightFieldCollider > heightFieldColliders
glm::vec4 color