载入中...
搜索中...
未找到
VolumeFluidEmitter.cpp
浏览该文件的文档.
2#include <algorithm>
3#include <cmath>
4#include <glm/geometric.hpp>
5#include <glm/gtc/quaternion.hpp>
6
7namespace eve::fluids {
8namespace {
9Result<unsigned> invalidEmission(const char* message) {
12}
13bool finiteVector(glm::vec3 v) { return std::isfinite(v.x) && std::isfinite(v.y) && std::isfinite(v.z); }
14bool valid(const VolumeFluidEmission& e) {
15 if (e.distribution.size() > 4096) return false;
16 for (const auto& p : e.distribution) {
17 const float directionLength = glm::length(p.direction);
18 if (!finiteVector(p.position) || !finiteVector(p.direction) || !std::isfinite(directionLength) ||
19 directionLength <= 1e-6f || !finiteVector(glm::vec3(p.color)) || !std::isfinite(p.color.a) ||
20 p.color.r < 0.f || p.color.g < 0.f || p.color.b < 0.f || p.color.a < 0.f || p.color.r > 1.f ||
21 p.color.g > 1.f || p.color.b > 1.f || p.color.a > 1.f)
22 return false;
23 }
24 const float length = glm::length(e.direction);
25 return finiteVector(e.origin) && finiteVector(e.direction) && std::isfinite(length) && length > 1e-6f &&
26 finiteVector(e.extent) && e.extent.x >= 0.f && e.extent.y >= 0.f && e.extent.z >= 0.f &&
27 e.extent.x <= 10000.f && e.extent.y <= 10000.f && e.extent.z <= 10000.f && std::isfinite(e.speed) &&
28 e.speed >= 0.f && std::isfinite(e.jitter) && e.jitter >= 0.f && std::isfinite(e.randomVelocity) &&
29 e.randomVelocity >= 0.f && e.randomVelocity <= 1.f && std::isfinite(e.granularRadiusRandomness) &&
30 e.granularRadiusRandomness >= 0.f && e.granularRadiusRandomness <= 100.f && e.actorCapacity >= 1 &&
31 e.actorCapacity <= 65536 && e.speed + std::sqrt(3.f) * e.jitter <= 100.f &&
33}
34
35template <class Controller>
36Result<void> restoreCheckpoint(VolumeFluid& solver, Controller& controller,
37 const VolumeFluidEmitterCheckpoint& checkpoint) {
38 if (checkpoint.schema != "eve.volume-fluid-emitter-checkpoint" || checkpoint.version != 1)
40 Diagnostic::error(DiagnosticCode::InvalidArgument, "Unsupported emitter checkpoint schema/version", "fluids.volume.emitterCheckpoint"));
41 if (!valid(checkpoint.emission)) return Result<void>::failure(
42 Diagnostic::error(DiagnosticCode::InvalidArgument, "Invalid checkpoint emission description", "fluids.volume.emitterCheckpoint"));
43 auto candidateResult = VolumeFluid::create(checkpoint.solver.settings);
44 if (!candidateResult) return Result<void>::failure(candidateResult.status());
45 auto candidate = std::move(candidateResult).takeValue();
46 auto solverValid = candidate->restore(checkpoint.solver);
47 if (!solverValid) return solverValid;
48 Controller controllerCandidate;
49 auto controllerValid = controllerCandidate.restore(checkpoint.controller);
50 if (!controllerValid) return controllerValid;
51 auto solverRestored = solver.restore(checkpoint.solver);
52 if (!solverRestored) return solverRestored;
53 auto controllerRestored = controller.restore(checkpoint.controller);
54 if (!controllerRestored) return controllerRestored;
55 return Result<void>::success();
56}
57
58float sample(uint32_t& state);
59void randomizeGranularRadius(VolumeFluidParticle& particle, float spacing, float randomness, uint32_t& rng);
60
61Result<unsigned> emitBurst(VolumeFluid& solver, const VolumeFluidEmission& e, unsigned count, uint32_t sequence,
62 size_t distributionOffset, size_t actorFree, float stepOffset, float dt) {
63 if (!valid(e) || count > 4096) return invalidEmission("Invalid nozzle or burst work limit");
64 if (count > 0 && e.shape == VolumeFluidEmissionShape::Distribution && e.distribution.empty())
65 return invalidEmission("Cannot emit a nonempty burst from an empty distribution");
66 if (count > solver.availableCapacity()) return invalidEmission("Burst exceeds available pool");
67 if (count > actorFree) return invalidEmission("Burst exceeds emitter actor capacity");
68 const auto forward = glm::normalize(e.direction);
69 const auto reference = std::abs(forward.y) < 0.9f ? glm::vec3(0, 1, 0) : glm::vec3(1, 0, 0);
70 const auto right = glm::normalize(glm::cross(reference, forward));
71 const auto up = glm::cross(forward, right);
72 const auto nozzleRotation = glm::quat_cast(glm::mat3(right, up, forward));
73 const auto prototypeRotation = glm::quat(e.prototype.orientation.w, e.prototype.orientation.x,
74 e.prototype.orientation.y, e.prototype.orientation.z);
75 uint32_t rng = e.seed + sequence * 0x9e3779b9u;
76 std::vector<VolumeFluidParticle> particles;
77 particles.reserve(count);
78 for (unsigned i = 0; i < count; ++i) {
79 const float a = sample(rng), b = sample(rng), c = sample(rng);
80 glm::vec3 local(0.f);
81 const VolumeFluidDistributionPoint* distributionPoint = nullptr;
82 switch (e.shape) {
84 distributionPoint = &e.distribution[(distributionOffset + i) % e.distribution.size()];
85 local = distributionPoint->position;
86 break;
87 case VolumeFluidEmissionShape::Edge: local.x = (2.f * a - 1.f) * e.extent.x; break;
89 local = {(2.f * a - 1.f) * e.extent.x, (2.f * b - 1.f) * e.extent.y, 0.f};
90 break;
92 local = {(2.f * a - 1.f) * e.extent.x, (2.f * b - 1.f) * e.extent.y, (2.f * c - 1.f) * e.extent.z};
93 break;
95 const float radius = std::sqrt(a) * e.extent.x, angle = 6.28318530718f * b;
96 local = {radius * std::cos(angle), radius * std::sin(angle), 0.f};
97 break;
98 }
100 const float z = 2.f * a - 1.f, angle = 6.28318530718f * b;
101 const float xy = std::sqrt(std::max(0.f, 1.f - z * z)), radius = std::cbrt(c) * e.extent.x;
102 local = radius * glm::vec3(xy * std::cos(angle), xy * std::sin(angle), z);
103 break;
104 }
105 }
106 auto particle = e.prototype;
107 randomizeGranularRadius(particle, solver.spacing(), e.granularRadiusRandomness, rng);
108 const auto worldOrientation = glm::normalize(nozzleRotation * prototypeRotation);
109 particle.orientation = {worldOrientation.x, worldOrientation.y, worldOrientation.z, worldOrientation.w};
110 if (distributionPoint && e.useShapeColor) particle.color = distributionPoint->color;
111 particle.position = e.origin + right * local.x + up * local.y + forward * local.z;
112 const float jx = 2.f * sample(rng) - 1.f, jy = 2.f * sample(rng) - 1.f, jz = 2.f * sample(rng) - 1.f;
113 const auto emissionDirection =
114 distributionPoint ? nozzleRotation * glm::normalize(distributionPoint->direction) : forward;
115 const float randomZ = jx;
116 const float randomAngle = 3.14159265359f * (jy + 1.f);
117 const float randomXY = std::sqrt(std::max(0.f, 1.f - randomZ * randomZ));
118 const auto randomDirection =
119 glm::vec3(randomXY * std::cos(randomAngle), randomXY * std::sin(randomAngle), randomZ);
120 const auto spawnDirection = emissionDirection * (1.f - e.randomVelocity) + randomDirection * e.randomVelocity;
121 particle.velocity = spawnDirection * e.speed + e.jitter * glm::vec3(jx, jy, jz);
122 particle.position += spawnDirection * e.speed * dt * stepOffset;
123 particles.push_back(particle);
124 }
125 auto admitted = solver.emit(particles);
126 if (!admitted) return Result<unsigned>::failure(admitted.status());
128}
129// A fixed integer permutation makes sample cost bounded (no rejection sampling).
130float sample(uint32_t& state) {
131 state += 0x9e3779b9u;
132 uint32_t x = state;
133 x = (x ^ (x >> 16u)) * 0x85ebca6bu;
134 x = (x ^ (x >> 13u)) * 0xc2b2ae35u;
135 x ^= x >> 16u;
136 return float(x >> 8u) * (1.f / 16777216.f);
137}
138void randomizeGranularRadius(VolumeFluidParticle& particle, float spacing, float randomness, uint32_t& rng) {
139 if (randomness == 0.f || particle.material.phase != VolumeFluidPhase::Granular) return;
140 const float radius = std::max(.001f, spacing * .5f + .001f - sample(rng) * spacing * (randomness * .01f));
141 particle.radii = glm::vec3(radius);
142}
143} // namespace
144
146 float smoothing) {
147 if (!std::isfinite(resolution) || resolution < .001f || resolution > 1000000.f || !std::isfinite(restDensity) ||
148 restDensity < .001f || restDensity > 1000000.f || !std::isfinite(smoothing) || smoothing < 1.f ||
149 smoothing > 100.f)
151 DiagnosticCode::InvalidArgument, "Invalid 3D emitter blueprint metrics", "fluids.volume.emitterBlueprint"));
153 metrics.particleSize = 1.f / (10.f * std::cbrt(resolution));
154 metrics.particleMass = restDensity * metrics.particleSize * metrics.particleSize * metrics.particleSize;
155 metrics.smoothingRadius = metrics.particleSize * smoothing;
157}
158
160 const VolumeFluidEmitterBlueprint3D& blueprint) {
161 const auto metrics =
163 const auto finiteRange = [](float value, float minimum, float maximum) {
164 return std::isfinite(value) && value >= minimum && value <= maximum;
165 };
166 if (!metrics || blueprint.capacity == 0 || blueprint.capacity > 65536 ||
167 !finiteRange(blueprint.viscosity, 0.f, 100.f) || !finiteRange(blueprint.surfaceTension, 0.f, 10.f) ||
168 !finiteRange(blueprint.buoyancy, -10.f, 10.f) || !finiteRange(blueprint.atmosphericDrag, 0.f, 100.f) ||
169 !finiteRange(blueprint.atmosphericPressure, 0.f, 1000.f) || !finiteRange(blueprint.vorticity, 0.f, 10.f) ||
170 !finiteRange(blueprint.diffusion, 0.f, 100.f) || !finiteVector(glm::vec3(blueprint.diffusionData)) ||
171 !std::isfinite(blueprint.diffusionData.w))
173 DiagnosticCode::InvalidArgument, "Invalid 3D fluid emitter blueprint", "fluids.volume.emitterBlueprint"));
174
176 result.metrics = metrics.value();
177 result.settings.capacity = blueprint.capacity;
178 result.settings.spacing = result.metrics.particleSize;
179 result.emission.actorCapacity = blueprint.capacity;
180 auto& material = result.emission.prototype.material;
182 material.density = blueprint.restDensity;
183 material.smoothing = blueprint.smoothing;
184 material.viscosity = blueprint.viscosity;
185 material.cohesion = blueprint.surfaceTension;
186 material.buoyancy = blueprint.buoyancy;
187 material.drag = blueprint.atmosphericDrag;
188 material.atmosphericPressure = blueprint.atmosphericPressure;
189 material.vorticity = blueprint.vorticity;
190 material.diffusion = blueprint.diffusion;
191 result.emission.prototype.data = blueprint.diffusionData;
193}
194
196 const VolumeGranularEmitterBlueprint3D& blueprint) {
197 const auto metrics = evaluateVolumeFluidEmitterBlueprint3D(blueprint.resolution, blueprint.restDensity, 1.f);
198 if (!metrics || blueprint.capacity == 0 || blueprint.capacity > 65536 || !std::isfinite(blueprint.randomness) ||
199 blueprint.randomness < 0.f || blueprint.randomness > 100.f)
201 Diagnostic::error(DiagnosticCode::InvalidArgument, "Invalid 3D granular emitter blueprint",
202 "fluids.volume.granularEmitterBlueprint"));
203
205 result.metrics = metrics.value();
206 result.metrics.smoothingRadius = 0.f;
207 result.settings.capacity = blueprint.capacity;
208 result.settings.spacing = result.metrics.particleSize;
209 result.emission.actorCapacity = blueprint.capacity;
212 result.emission.prototype.material.density = blueprint.restDensity;
214}
215
217 if (!valid(e)) return invalidEmission("Invalid emission description");
219 if (!active) return Result<unsigned>::failure(active.status());
220 const size_t actorFree = e.actorCapacity > active.value() ? e.actorCapacity - active.value() : 0;
221 return emitBurst(solver, e, count, 0, 0, actorFree, 0.f, 0.f);
222}
223
225 float offset, float dt) {
226 if (!valid(emission) || !std::isfinite(offset) || offset < 0.f || offset > 1.f || !std::isfinite(dt) || dt < 0.f ||
227 dt > 1.f / 30.f)
228 return invalidEmission("Invalid direct particle emission offset or duration");
229 auto active = solver.actorParticleCount(emission.prototype.actorGroup);
230 if (!active) return Result<unsigned>::failure(active.status());
231 const size_t actorFree = emission.actorCapacity > active.value() ? emission.actorCapacity - active.value() : 0;
232 if (actorFree == 0 || solver.availableCapacity() == 0 ||
233 (emission.shape == VolumeFluidEmissionShape::Distribution && emission.distribution.empty())) {
234 emitting_ = false;
236 }
237 auto result = emitBurst(solver, emission, 1, sequence_,
238 emission.distribution.empty() ? 0 : size_t(sequence_) % emission.distribution.size(),
239 actorFree, offset, dt);
240 if (!result) return result;
241 ++sequence_;
242 emitting_ = true;
243 return result;
244}
245
247 float rate, unsigned maxPerStep, float minimumPoolFraction) {
248 if (!valid(emission) || !std::isfinite(dt) || dt < 0.f || dt > 1.f / 30.f || !std::isfinite(rate) || rate < 0.f ||
249 rate > 1000000.f || maxPerStep == 0 || maxPerStep > 4096 || !std::isfinite(minimumPoolFraction) ||
250 minimumPoolFraction < 0.f || minimumPoolFraction > 1.f)
251 return invalidEmission("Invalid stream duration, rate, threshold or work limit");
252 // Fluid3D's runtime controllers stop the emitter by setting speed to zero.
253 // Discard fractional credit so re-enabling does not catch up disabled time.
254 if (emission.speed == 0.f) {
255 credit_ = 0.0;
257 }
258 auto active = solver.actorParticleCount(emission.prototype.actorGroup);
259 if (!active) return Result<unsigned>::failure(active.status());
260 const size_t free = solver.availableCapacity();
261 const size_t actorFree = emission.actorCapacity > active.value() ? emission.actorCapacity - active.value() : 0;
262 if ((emission.shape == VolumeFluidEmissionShape::Distribution && emission.distribution.empty()) || free == 0 ||
263 actorFree == 0 ||
264 (!emitting_ && double(actorFree) <= std::floor(double(emission.actorCapacity) * minimumPoolFraction))) {
265 credit_ = 0.0;
266 emitting_ = false;
268 }
269 const double accumulated = credit_ + double(dt) * rate;
270 const double whole = std::floor(accumulated);
271 const unsigned count = unsigned(std::min({whole, double(maxPerStep), double(free), double(actorFree)}));
272 auto result = emitBurst(solver, emission, count, sequence_,
273 emission.distribution.empty() ? 0 : size_t(sequence_) % emission.distribution.size(),
274 actorFree, 0.f, 0.f);
275 if (!result) return result;
276 credit_ = accumulated - whole;
277 sequence_ += count;
278 emitting_ = count != free && count != actorFree;
279 return result;
280}
281
283 unsigned count, float minimumPoolFraction) {
284 if (!valid(emission) || count == 0 || count > 4096 || !std::isfinite(minimumPoolFraction) ||
285 minimumPoolFraction < 0.f || minimumPoolFraction > 1.f)
286 return invalidEmission("Invalid burst count, threshold or emission description");
287 auto active = solver.actorParticleCount(emission.prototype.actorGroup);
288 if (!active) return Result<unsigned>::failure(active.status());
289 if (active.value() != 0) {
290 emitting_ = true;
292 }
293 const size_t free = solver.availableCapacity();
294 if (count > emission.actorCapacity || free < count ||
295 double(emission.actorCapacity) <= std::floor(double(emission.actorCapacity) * minimumPoolFraction)) {
296 emitting_ = false;
298 }
299 auto result = emitBurst(solver, emission, count, sequence_, 0, emission.actorCapacity, 0.f, 0.f);
300 if (!result) return result;
301 sequence_ += count;
302 emitting_ = true;
303 return result;
304}
305} // namespace eve::fluids
306
307namespace eve::fluids {
309 unsigned maxPerStep, float minimumPoolFraction) {
310 if (!valid(e)) return invalidEmission("Invalid jet description");
311 const auto forward = glm::normalize(e.direction);
312 const auto reference = std::abs(forward.y) < .9f ? glm::vec3(0, 1, 0) : glm::vec3(1, 0, 0);
313 const auto right = glm::normalize(glm::cross(reference, forward));
314 const auto rotation = glm::quat_cast(glm::mat3(right, glm::cross(forward, right), forward));
316 return advanceMoving(solver, e, pose, pose, dt, maxPerStep, minimumPoolFraction, 0.f);
317}
318
321 const VolumeFluidNozzlePose& end, float dt, unsigned maxPerStep,
322 float minimumPoolFraction, float inheritVelocity) {
323 const auto validPose = [](const VolumeFluidNozzlePose& pose) {
324 const float norm = glm::dot(pose.rotation, pose.rotation);
325 return finiteVector(pose.position) && std::isfinite(norm) && std::abs(norm - 1.f) < .001f;
326 };
327 if (!validPose(begin) || !validPose(end) || !std::isfinite(inheritVelocity) || inheritVelocity < 0.f ||
328 inheritVelocity > 1.f)
329 return invalidEmission("Invalid nozzle pose or inherited velocity fraction");
331 !std::isfinite(dt) || dt < 0.f || dt > 1.f / 30.f || maxPerStep == 0 || maxPerStep > 4096 ||
332 !std::isfinite(minimumPoolFraction) || minimumPoolFraction < 0.f || minimumPoolFraction > 1.f)
333 return invalidEmission("Invalid jet shape or timestep/work limit");
334 if (e.speed == 0.f) {
335 distance_ = 0.0;
337 }
339 if (!active) return Result<unsigned>::failure(active.status());
340 const size_t actorFree = e.actorCapacity > active.value() ? e.actorCapacity - active.value() : 0;
341 if (actorFree == 0 ||
342 (!emitting_ && double(actorFree) <= std::floor(double(e.actorCapacity) * minimumPoolFraction))) {
343 distance_ = 0.0;
344 emitting_ = false;
346 }
347 const float spacing = solver.spacing();
348 std::span<const VolumeFluidDistributionPoint> points;
351 if (points.empty()) {
352 distance_ = 0.0;
353 emitting_ = false;
355 }
356 } else {
357 const double nx = std::floor(double(e.extent.x) / spacing);
358 const double ny =
360 ? 0.0
361 : std::floor(double(e.shape == VolumeFluidEmissionShape::Disk ? e.extent.x : e.extent.y) / spacing);
362 if ((2.0 * nx + 1.0) * (2.0 * ny + 1.0) > 4096.0)
363 return invalidEmission("Jet lattice exceeds 4096 candidate points");
364 layerPoints_.clear();
365 layerPoints_.reserve(size_t((2.0 * nx + 1.0) * (2.0 * ny + 1.0)));
366 for (int y = -int(ny); y <= int(ny); ++y)
367 for (int x = -int(nx); x <= int(nx); ++x) {
368 const glm::vec3 point(float(x) * spacing, float(y) * spacing, 0.f);
369 if (e.shape == VolumeFluidEmissionShape::Disk && glm::dot(point, point) > e.extent.x * e.extent.x)
370 continue;
371 layerPoints_.push_back({point, glm::vec4(1.f)});
372 }
373 points = layerPoints_;
374 }
375 if (points.size() > maxPerStep) return invalidEmission("Jet work cap cannot fit a complete nozzle layer");
376 const size_t free = solver.availableCapacity();
377 if (free < points.size() || actorFree < points.size()) {
378 distance_ = 0.0;
379 emitting_ = false;
381 }
382 const double travelled = distance_ + double(e.speed) * dt;
383 const double requested = std::floor(travelled / spacing);
384 const unsigned layers = unsigned(std::min({requested, double(maxPerStep / points.size()),
385 double(free / points.size()), double(actorFree / points.size())}));
386 const double remainder = travelled - requested * spacing;
387 const auto rotation = [](glm::vec4 q) { return glm::normalize(glm::quat(q.w, q.x, q.y, q.z)); };
388 const auto q0 = rotation(begin.rotation), q1 = rotation(end.rotation);
389 const auto prototypeRotation = rotation(e.prototype.orientation);
390 auto difference = q1 * glm::conjugate(q0);
391 if (difference.w < 0.f) difference = -difference;
392 const float angle = 2.f * std::acos(std::clamp(difference.w, -1.f, 1.f));
393 const float sine = glm::length(glm::vec3(difference.x, difference.y, difference.z));
394 const auto omega = dt > 0.f && sine > 1e-6f
395 ? glm::vec3(difference.x, difference.y, difference.z) * (angle / (sine * dt))
396 : glm::vec3(0.f);
397 const auto linear = dt > 0.f ? (end.position - begin.position) / dt : glm::vec3(0.f);
398 std::vector<VolumeFluidParticle> batch;
399 batch.reserve(layers * points.size());
400 uint32_t rng = e.seed + sequence_ * 0x9e3779b9u;
401 for (unsigned layer = 0; layer < layers; ++layer) {
402 const float distance = float(remainder + double(layer) * spacing);
403 const float age = e.speed > 0.f ? distance / e.speed : 0.f;
404 const float fraction = dt > 0.f ? std::clamp(1.f - age / dt, 0.f, 1.f) : 1.f;
405 const auto orientation = glm::slerp(q0, q1, fraction);
406 const auto origin = glm::mix(begin.position, end.position, fraction);
407 for (const auto& point : points) {
408 auto p = e.prototype;
409 randomizeGranularRadius(p, spacing, e.granularRadiusRandomness, rng);
410 const auto worldOrientation = glm::normalize(orientation * prototypeRotation);
411 p.orientation = {worldOrientation.x, worldOrientation.y, worldOrientation.z, worldOrientation.w};
412 const auto offset = orientation * point.position;
413 const float jx = 2.f * sample(rng) - 1.f, jy = 2.f * sample(rng) - 1.f, jz = 2.f * sample(rng) - 1.f;
414 const auto emissionDirection = orientation * glm::normalize(point.direction);
415 p.velocity = emissionDirection * e.speed + e.jitter * glm::vec3(jx, jy, jz) +
416 inheritVelocity * (linear + glm::cross(omega, offset));
417 p.position = origin + offset + p.velocity * age;
418 if (e.useShapeColor) p.color = point.color;
419 batch.push_back(p);
420 }
421 }
422 auto admitted = solver.emit(batch);
423 if (!admitted) return Result<unsigned>::failure(admitted.status());
424 distance_ = remainder;
425 if (layers != 0) ++sequence_;
426 emitting_ = free - batch.size() >= points.size() && actorFree - batch.size() >= points.size();
427 return Result<unsigned>::success(unsigned(batch.size()));
428}
429} // namespace eve::fluids
430
431namespace eve::fluids {
432namespace {
433using DistributionResult = Result<std::vector<VolumeFluidDistributionPoint>>;
434DistributionResult distributionFailure(const char* message, const char* source) {
435 return DistributionResult::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, message, source));
436}
437bool appendDistributionPoint(std::vector<VolumeFluidDistributionPoint>& output, const glm::vec3& position,
438 const glm::vec3& direction) {
439 if (output.size() >= 4096) return false;
440 output.push_back({position, glm::vec4(1.f), direction});
441 return true;
442}
443} // namespace
445 bool surface) {
446 constexpr const char* source = "fluids.volume.sphereDistribution";
447 if (!std::isfinite(radius) || radius <= 0.f || !std::isfinite(spacing) || spacing <= 0.f)
448 return distributionFailure("Sphere radius and spacing must be finite and positive", source);
449 std::vector<VolumeFluidDistributionPoint> output;
450 if (!surface) {
451 const double ratio = double(radius) / spacing;
452 const double countValue = std::ceil(ratio - std::max(1.0, ratio) * 1e-7);
453 if (countValue <= 0.0 || countValue > 32767.0)
454 return distributionFailure("Sphere lattice exceeds 65536 candidates", source);
455 const uint64_t side = uint64_t(2.0 * countValue + 1.0);
456 if (side * side * side > 65536) return distributionFailure("Sphere lattice exceeds 65536 candidates", source);
457 const int count = int(countValue);
458 const float step = radius / float(count);
459 for (int z = -count; z <= count; ++z)
460 for (int y = -count; y <= count; ++y)
461 for (int x = -count; x <= count; ++x) {
462 const glm::vec3 point(float(x) * step, float(y) * step, float(z) * step);
463 if (glm::length(point) < radius && !appendDistributionPoint(output, point, {0, 0, 1}))
464 return distributionFailure("Sphere distribution exceeds 4096 points", source);
465 }
466 return DistributionResult::success(std::move(output));
467 }
468 if (spacing > 2.f * radius) {
469 output.push_back({{0, 0, radius}, glm::vec4(1.f), {0, 0, 1}});
470 return DistributionResult::success(std::move(output));
471 }
472 const double increment = 2.0 * std::asin(std::clamp(double(spacing) / (2.0 * radius), 0.0, 1.0));
473 const double ringValue = std::ceil(3.141592653589793 / increment);
474 if (!std::isfinite(ringValue) || ringValue > 65535.0)
475 return distributionFailure("Sphere lattice exceeds 65536 candidates", source);
476 const int rings = std::max(1, int(ringValue));
477 uint64_t candidates = 0;
478 for (int ring = 0; ring <= rings; ++ring) {
479 const double polar = 3.141592653589793 * ring / rings;
480 const float ringRadius = float(std::sin(polar) * radius), z = float(std::cos(polar) * radius);
481 const double sampleValue = std::ceil(2.0 * 3.141592653589793 * ringRadius / spacing);
482 if (!std::isfinite(sampleValue) || sampleValue > 65536.0)
483 return distributionFailure("Sphere lattice exceeds 65536 candidates", source);
484 const int samples = ring == 0 || ring == rings ? 1 : std::max(1, int(sampleValue));
485 candidates += unsigned(samples);
486 if (candidates > 65536) return distributionFailure("Sphere lattice exceeds 65536 candidates", source);
487 for (int sample = 0; sample < samples; ++sample) {
488 const float angle = float(2.0 * 3.141592653589793 * sample / samples);
489 const glm::vec3 point(ringRadius * std::cos(angle), ringRadius * std::sin(angle), z);
490 if (!appendDistributionPoint(output, point, glm::normalize(point)))
491 return distributionFailure("Sphere distribution exceeds 4096 points", source);
492 }
493 }
494 return DistributionResult::success(std::move(output));
495}
496
498 bool surface) {
499 constexpr const char* source = "fluids.volume.cubeDistribution";
500 if (!finiteVector(size) || glm::any(glm::lessThanEqual(size, glm::vec3(0.f))) || !std::isfinite(spacing) ||
501 spacing <= 0.f)
502 return distributionFailure("Cube size and spacing must be finite and positive", source);
503 const glm::dvec3 ratio = glm::dvec3(size) / double(spacing);
504 const glm::dvec3 countValue = glm::ceil(ratio - glm::max(glm::dvec3(1.0), ratio) * 1e-7);
505 if (glm::any(glm::greaterThan(countValue, glm::dvec3(65535.0))))
506 return distributionFailure("Cube lattice exceeds 65536 candidates", source);
507 const glm::ivec3 count = glm::max(glm::ivec3(1), glm::ivec3(countValue));
508 const uint64_t candidates = uint64_t(count.x + 1) * uint64_t(count.y + 1) * uint64_t(count.z + 1);
509 if (candidates > 65536) return distributionFailure("Cube lattice exceeds 65536 candidates", source);
510 std::vector<VolumeFluidDistributionPoint> output;
511 output.reserve(size_t(std::min<uint64_t>(candidates, 4096)));
512 for (int z = 0; z <= count.z; ++z)
513 for (int y = 0; y <= count.y; ++y)
514 for (int x = 0; x <= count.x; ++x) {
515 if (surface && x != 0 && x != count.x && y != 0 && y != count.y && z != 0 && z != count.z) continue;
516 const glm::vec3 point(float(x) / count.x - .5f, float(y) / count.y - .5f, float(z) / count.z - .5f);
517 glm::vec3 direction(0, 0, 1);
518 if (surface) {
519 direction = {x == 0 ? -1.f : (x == count.x ? 1.f : 0.f), y == 0 ? -1.f : (y == count.y ? 1.f : 0.f),
520 z == 0 ? -1.f : (z == count.z ? 1.f : 0.f)};
521 direction = glm::normalize(direction);
522 }
523 if (!appendDistributionPoint(output, point * size, direction))
524 return distributionFailure("Cube distribution exceeds 4096 points", source);
525 }
526 return DistributionResult::success(std::move(output));
527}
528
530 float radialVelocityDegrees) {
531 constexpr const char* source = "fluids.volume.edgeDistribution";
532 if (!std::isfinite(length) || length < 0.f || length > 10000.f || !std::isfinite(spacing) || spacing <= 0.f ||
533 !std::isfinite(radialVelocityDegrees) || std::abs(radialVelocityDegrees) > 360000.f)
534 return distributionFailure("Edge length, spacing or radial velocity is invalid", source);
535 const double separation = double(spacing) + .01;
536 const double amountValue = std::floor(double(length) / separation);
537 if (amountValue > 4095.0) return distributionFailure("Edge distribution exceeds 4096 points", source);
538 const int amount = int(amountValue);
539 std::vector<VolumeFluidDistributionPoint> output;
540 output.reserve(size_t(amount) + 1u);
541 for (int i = 0; i <= amount; ++i) {
542 const float angle = glm::radians(float(i) * radialVelocityDegrees);
543 const glm::vec3 direction(0.f, -std::sin(angle), std::cos(angle));
544 output.push_back({{float(double(i) * separation - double(length) * .5), 0, 0}, glm::vec4(1), direction});
545 }
546 return DistributionResult::success(std::move(output));
547}
548
550 bool edgeEmission) {
551 constexpr const char* source = "fluids.volume.diskDistribution";
552 if (!std::isfinite(radius) || radius <= 0.f || radius > 10000.f || !std::isfinite(spacing) || spacing <= 0.f)
553 return distributionFailure("Disk radius and spacing must be finite and positive", source);
554 std::vector<VolumeFluidDistributionPoint> output;
555 const int rings = edgeEmission ? 1 : int(std::floor(double(radius) / spacing));
556 if (!edgeEmission) output.push_back({{0, 0, 0}, glm::vec4(1), {0, 0, 1}});
557 uint64_t candidates = output.size();
558 for (int ring = edgeEmission ? 1 : 1; ring <= rings; ++ring) {
559 const float r = edgeEmission ? radius : spacing * float(ring);
560 const double ratio = std::clamp(double(spacing) / (2.0 * r), 0.0, 1.0);
561 const double increment = 2.0 * std::asin(ratio);
562 const double stepsValue = increment > 0.0 ? 2.0 * 3.141592653589793 / increment : 65537.0;
563 if (!std::isfinite(stepsValue) || stepsValue > 65536.0)
564 return distributionFailure("Disk lattice exceeds 65536 candidates", source);
565 const int steps = std::max(1, int(std::ceil(stepsValue)));
566 candidates += unsigned(steps);
567 if (candidates > 65536) return distributionFailure("Disk lattice exceeds 65536 candidates", source);
568 for (int sample = 0; sample < steps; ++sample) {
569 const float angle = float(2.0 * 3.141592653589793 * sample / steps);
570 const glm::vec3 point(r * std::cos(angle), r * std::sin(angle), 0.f);
571 const glm::vec3 direction = edgeEmission ? glm::normalize(point) : glm::vec3(0, 0, 1);
572 if (!appendDistributionPoint(output, point, direction))
573 return distributionFailure("Disk distribution exceeds 4096 points", source);
574 }
575 }
576 return DistributionResult::success(std::move(output));
577}
578
580 std::span<const VolumeFluidEmission> shapes) {
581 constexpr const char* source = "fluids.volume.emitterShapes";
582 if (shapes.size() > 64)
584 Diagnostic::error(DiagnosticCode::InvalidArgument, "Emitter shape count exceeds 64", source));
585
586 VolumeFluidEmission combined = base;
588 combined.origin = glm::vec3(0.f);
589 combined.direction = glm::vec3(0.f, 0.f, 1.f);
590 combined.distribution.clear();
591
592 const auto appendPoint = [&](glm::vec3 position, glm::vec3 direction, glm::vec4 color) {
593 if (combined.distribution.size() == 4096) return false;
594 combined.distribution.push_back({position, color, direction});
595 return true;
596 };
597 const auto appendShape = [&](const VolumeFluidEmission& shape) {
598 if (!valid(shape) || shape.shape != VolumeFluidEmissionShape::Distribution) return false;
599 const auto forward = glm::normalize(shape.direction);
600 const auto reference = std::abs(forward.y) < .9f ? glm::vec3(0, 1, 0) : glm::vec3(1, 0, 0);
601 const auto right = glm::normalize(glm::cross(reference, forward));
602 const auto up = glm::cross(forward, right);
603 if (shape.distribution.empty()) return appendPoint(shape.origin, forward, glm::vec4(1.f));
604 for (const auto& point : shape.distribution) {
605 const auto position =
606 shape.origin + right * point.position.x + up * point.position.y + forward * point.position.z;
607 const auto direction =
608 glm::normalize(right * point.direction.x + up * point.direction.y + forward * point.direction.z);
609 if (!appendPoint(position, direction, point.color)) return false;
610 }
611 return true;
612 };
613
614 if (shapes.empty()) {
615 if (!valid(base) || !appendPoint(base.origin, glm::normalize(base.direction), glm::vec4(1.f)))
617 Diagnostic::error(DiagnosticCode::InvalidArgument, "Invalid emitter-wide description", source));
618 } else {
619 for (const auto& shape : shapes)
620 if (!appendShape(shape))
623 "Invalid emitter shape or distribution exceeds 4096 points", source));
624 }
625 if (!valid(combined))
627 Diagnostic::error(DiagnosticCode::InvalidArgument, "Invalid emitter-wide description", source));
628 return Result<VolumeFluidEmission>::success(std::move(combined));
629}
630} // namespace eve::fluids
631
632namespace eve::fluids {
634 unsigned width, unsigned height,
635 float pixelScale, float maximumSize,
636 float spacing,
637 float alphaThreshold) {
639 const auto fail = [](const char* text) {
640 return Output::failure(
641 Diagnostic::error(DiagnosticCode::InvalidArgument, text, "fluids.volume.imageDistribution"));
642 };
643 if (width == 0 || height == 0 || uint64_t(width) * height != pixels.size() || pixels.size() > 16777216 ||
644 !std::isfinite(pixelScale) || pixelScale <= 0.f || !std::isfinite(maximumSize) || maximumSize <= 0.f ||
645 !std::isfinite(spacing) || spacing <= 0.f || !std::isfinite(alphaThreshold) || alphaThreshold < 0.f ||
646 alphaThreshold > 1.f)
647 return fail("Invalid image dimensions, scale, spacing or alpha threshold");
648 for (const auto pixel : pixels)
649 for (int i = 0; i < 4; ++i)
650 if (!std::isfinite(pixel[i]) || pixel[i] < 0.f || pixel[i] > 1.f)
651 return fail("Pixels must be finite linear RGBA in [0,1]");
652 double worldWidth = double(width) * pixelScale, worldHeight = double(height) * pixelScale;
653 const double scale = std::min(1.0, double(maximumSize) / std::max(worldWidth, worldHeight));
654 worldWidth *= scale;
655 worldHeight *= scale;
656 const double countX = std::floor(worldWidth / spacing), countY = std::floor(worldHeight / spacing);
657 if (countX > 65536 || countY > 65536 || countX * countY > 65536)
658 return fail("Image lattice exceeds 65536 candidates");
659 std::vector<VolumeFluidDistributionPoint> points;
660 const unsigned nx = unsigned(countX), ny = unsigned(countY);
661 for (unsigned x = 0; x < nx; ++x)
662 for (unsigned y = 0; y < ny; ++y) {
663 // Clamp-addressed bilinear sampling at normalized texture coordinates.
664 const double sx = std::clamp(double(x) / nx * width - .5, 0.0, double(width - 1));
665 const double sy = std::clamp(double(y) / ny * height - .5, 0.0, double(height - 1));
666 const unsigned x0 = unsigned(sx), y0 = unsigned(sy), x1 = std::min(x0 + 1, width - 1),
667 y1 = std::min(y0 + 1, height - 1);
668 const float fx = float(sx - x0), fy = float(sy - y0);
669 const auto lower = pixels[size_t(y0) * width + x0] * (1.f - fx) + pixels[size_t(y0) * width + x1] * fx;
670 const auto upper = pixels[size_t(y1) * width + x0] * (1.f - fx) + pixels[size_t(y1) * width + x1] * fx;
671 const auto color = lower * (1.f - fy) + upper * fy;
672 if (color.a <= alphaThreshold) continue;
673 if (points.size() == 4096) return fail("Image distribution exceeds 4096 emission points");
674 points.push_back(
675 {{float(double(x) * spacing - worldWidth * .5), float(double(y) * spacing - worldHeight * .5), 0.f},
676 color});
677 }
678 return Output::success(std::move(points));
679}
680
682 const VolumeFluidEmission& emission,
683 const VolumeFluidEmitter& controller) {
684 return {"eve.volume-fluid-emitter-checkpoint", 1, solver.snapshot(), emission, controller.snapshot()};
685}
686
688 const VolumeFluidEmission& emission,
689 const VolumeFluidJetEmitter& controller) {
690 return {"eve.volume-fluid-emitter-checkpoint", 1, solver.snapshot(), emission, controller.snapshot()};
691}
692
694 const VolumeFluidEmitterCheckpoint& checkpoint) {
695 return restoreCheckpoint(solver, controller, checkpoint);
696}
697
699 const VolumeFluidEmitterCheckpoint& checkpoint) {
700 return restoreCheckpoint(solver, controller, checkpoint);
701}
702} // namespace eve::fluids
703
704#include <iomanip>
705#include <limits>
706#include <locale>
707#include <sstream>
708#include <stdexcept>
709namespace eve::fluids {
710namespace {
711std::string phaseText(double value) {
712 std::ostringstream stream;
713 stream.imbue(std::locale::classic());
714 stream << std::setprecision(std::numeric_limits<double>::max_digits10) << value;
715 if (!stream) throw std::runtime_error("Emitter phase encoding failed");
716 return stream.str();
717}
718Result<double> restoredPhase(const VolumeFluidEmitterSnapshot& s, unsigned kind, double upper) {
719 std::istringstream stream(s.phase);
720 stream.imbue(std::locale::classic());
721 double phase = 0;
722 stream >> std::noskipws >> phase;
723 if (s.schema != "eve.volume-fluid-emitter-state" || s.version != 1 || s.kind != kind || stream.fail() ||
724 !stream.eof() || !std::isfinite(phase) || phase < 0 || phase >= upper)
726 "Invalid emitter state schema, kind or phase",
727 "fluids.volume.emitterState"));
729}
730} // namespace
732 return {"eve.volume-fluid-emitter-state", 1, 0, phaseText(credit_), sequence_, emitting_};
733}
735 auto phase = restoredPhase(state, 0, 1.0);
736 if (!phase) return Result<void>::failure(phase.status());
737 credit_ = phase.value();
738 sequence_ = state.sequence;
739 emitting_ = state.emitting;
740 return Result<void>::success();
741}
743 return {"eve.volume-fluid-emitter-state", 1, 1, phaseText(distance_), sequence_, emitting_};
744}
746 auto phase = restoredPhase(state, 1, 10.0);
747 if (!phase) return Result<void>::failure(phase.status());
748 distance_ = phase.value();
749 sequence_ = state.sequence;
750 emitting_ = state.emitting;
751 return Result<void>::success();
752}
753} // namespace eve::fluids
double value
bool & active
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
std::string output
eve::EntitySpatialPose pose
AuthorityStoreHandleRef reference
Definition Authority.cpp:24
const std::string & s
float phase
Definition CaveMesh.cpp:58
float length
Definition CaveMesh.cpp:94
float nx
float ny
glm::vec4 p[6]
std::string message
float maximum[3]
float minimum[3]
ShaderImageInput shape
std::array< double, 10 > q
double r
float v
HexVec3 up
HexVec3 right
std::int32_t c
std::uint32_t height
std::uint32_t width
std::string text
TokenKind kind
std::string local
size_t offset
std::array< float, 4 > rotation
std::array< float, 3 > position
std::array< float, 3 > scale
bool valid
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
float distance
std::unique_ptr< gpgpu::Sequence > sequence
Definition OnnxGpgpu.cpp:43
TileLayer * layer
float radius
std::shared_ptr< const std::vector< glm::vec2 > > points
float begin
int steps
uint8_t * pixels
Material * material
V3 origin
Definition RoadBake.cpp:138
RoadLaneDirection direction
std::uint32_t count
ecs::EntityHandle side
int spacing
float separation
Definition TreeMesh.cpp:159
float step
Definition TreeMesh.cpp:314
float size
Definition TreeMesh.cpp:156
const UnitySourceAsset & source
glm::vec3 point
float angle
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
Bounded runtime stream controller with explicit snapshot/restore.
VolumeFluidEmitterSnapshot snapshot() const
Copies controller state; caller checkpoints solver/configuration at the same step boundary.
Result< unsigned > emitParticle(VolumeFluid &solver, const VolumeFluidEmission &emission, float offset, float dt)
Emits one particle with Fluid3DEmitter's normalized intra-step position offset.
Result< unsigned > advanceBurst(VolumeFluid &solver, const VolumeFluidEmission &emission, unsigned count, float minimumPoolFraction=0.5f)
Advances Fluid3D's BURST emission lifecycle for one actor group.
Result< unsigned > advance(VolumeFluid &solver, const VolumeFluidEmission &emission, float dt, float rate, unsigned maxPerStep=256, float minimumPoolFraction=0.5f)
Advances emission with at most maxPerStep particles; excess whole-particle credit is discarded.
Result< void > restore(const VolumeFluidEmitterSnapshot &state)
Validates schema, kind and phase before atomic replacement; invokes no callbacks.
Spacing-driven planar/distribution jet controller with independent, owned stream phase.
Result< unsigned > advanceMoving(VolumeFluid &solver, const VolumeFluidEmission &emission, const VolumeFluidNozzlePose &begin, const VolumeFluidNozzlePose &end, float dt, unsigned maxPerStep=256, float minimumPoolFraction=0.5f, float inheritVelocity=0.f)
Emits using interpolated step-boundary rigid poses instead of description origin/direction.
Result< unsigned > advance(VolumeFluid &solver, const VolumeFluidEmission &emission, float dt, unsigned maxPerStep=256, float minimumPoolFraction=0.5f)
Emits whole nozzle layers spaced by solver.spacing().
Result< void > restore(const VolumeFluidEmitterSnapshot &state)
Validates schema, kind and phase before atomic replacement; invokes no callbacks.
VolumeFluidEmitterSnapshot snapshot() const
Copies controller state; caller checkpoints solver/configuration at the same step boundary.
CPU position-based free-volume fluid with a bounded spatial grid. @ownership Owns all particle state;...
float spacing() const
Returns the rest lattice spacing without copying state.
static Result< std::unique_ptr< VolumeFluid > > create(const VolumeFluidSettings &settings)
Validates settings before allocating an owning solver; errors publish no state.
size_t availableCapacity() const
Returns unused pool capacity in constant time without allocating.
VolumeFluidSnapshot snapshot() const
Returns complete owning state; scratch grids and last-step contacts are derived and omitted.
Result< unsigned > actorParticleCount(unsigned actorGroup) const
Counts current live particles for one Fluid3D-compatible actor group.
Result< void > emit(std::span< const VolumeFluidParticle > particles)
Atomically admits particles; rejects nonfinite, out-of-container or over-capacity input.
std::vector< double > forward(const Policy &p, const Observation &o)
Forward.
Definition Learning.h:65
GLSL compute kernels for the GPU surface-flow solver.
Definition FluidTarget.h:12
Result< std::vector< VolumeFluidDistributionPoint > > buildVolumeFluidEdgeDistribution(float length, float spacing, float radialVelocityDegrees)
Builds Fluid3D's edge lattice with per-sample radial velocity directions.
Result< std::vector< VolumeFluidDistributionPoint > > buildVolumeFluidCubeDistribution(glm::vec3 size, float spacing, bool surface)
Builds an Fluid3D-compatible box surface or volume lattice during setup.
VolumeFluidEmitterCheckpoint captureVolumeFluidEmitterCheckpoint(const VolumeFluid &solver, const VolumeFluidEmission &emission, const VolumeFluidEmitter &controller)
Captures one completed-step rate-emitter checkpoint without retaining references.
Result< void > restoreVolumeFluidEmitterCheckpoint(VolumeFluid &solver, VolumeFluidEmitter &controller, const VolumeFluidEmitterCheckpoint &checkpoint)
Atomically restores solver and rate controller after validating the complete owning candidate.
Result< std::vector< VolumeFluidDistributionPoint > > buildVolumeFluidDiskDistribution(float radius, float spacing, bool edgeEmission)
Builds Fluid3D's concentric disk or circumference-only distribution.
Result< VolumeFluidEmission > composeVolumeFluidEmitterShapes(const VolumeFluidEmission &base, std::span< const VolumeFluidEmission > shapes)
Builds one emitter distribution from an ordered set of Fluid3D emitter shapes.
Result< std::vector< VolumeFluidDistributionPoint > > buildVolumeFluidSphereDistribution(float radius, float spacing, bool surface)
Builds an Fluid3D-compatible spherical surface or volume lattice during setup.
Result< std::vector< VolumeFluidDistributionPoint > > buildVolumeFluidImageDistribution(std::span< const glm::vec4 > pixels, unsigned width, unsigned height, float pixelScale, float maximumSize, float spacing, float alphaThreshold)
Precomputes a bilinear alpha-masked image distribution on a spacing lattice.
Result< VolumeFluidEmitterBlueprintMetrics > evaluateVolumeFluidEmitterBlueprint3D(float resolution, float restDensity, float smoothing)
Evaluates Fluid3DEmitterBlueprintBase/Fluid3DEmitterBlueprint formulas in 3D.
Result< VolumeFluidEmitterBlueprintApplication3D > prepareVolumeFluidEmitterBlueprint3D(const VolumeFluidEmitterBlueprint3D &blueprint)
Atomically maps every Fluid3DEmitterBlueprint field to native 3D setup values.
Result< VolumeFluidEmitterBlueprintApplication3D > prepareVolumeGranularEmitterBlueprint3D(const VolumeGranularEmitterBlueprint3D &blueprint)
Atomically maps an Fluid3DGranularEmitterBlueprint to owned native 3D setup values.
Result< unsigned > emitVolumeFluidBurst(VolumeFluid &solver, const VolumeFluidEmission &e, unsigned count)
Generates at most 4096 particles and admits the entire burst atomically.
double sample(const Heightmap &map, double u, double v)
Sample.
Value-owned emission description; +Z is mapped to direction.
VolumeFluidParticle prototype
Material/color/data/lifetime copied to emitted particles; position/velocity are generated.
glm::vec3 extent
Half extents; disk/sphere use x as radius.
float speed
Nozzle speed and independent per-axis uniform native velocity jitter, m/s.
std::vector< VolumeFluidDistributionPoint > distribution
Precomputed shape points, used only by Distribution; maximum 4096, copied by value.
bool useShapeColor
Whether Distribution points replace prototype color, matching Fluid3DEmitter useShapeColor.
unsigned actorCapacity
Maximum live particles owned by this emitter actor, matching Fluid3DEmitterBlueprintBase capacity.
uint32_t seed
Explicit named emission RNG stream seed; never touches a global RNG.
float granularRadiusRandomness
Granular radius reduction percentage in [0,100]; zero preserves the prototype radius.
Owning Fluid3DEmitterBlueprint values used to prepare a native 3D emitter.
Fully owned native setup prepared from one Fluid3D-emitter blueprint.
Exact 3D metrics derived by Fluid3DEmitterBlueprintBase from resolution and density.
float particleMass
Per-particle rest mass in kilograms.
float smoothingRadius
Fluid support radius in metres.
float particleSize
Recommended solver rest spacing and particle diameter in metres.
Version-1 owning atomic checkpoint for a solver, emission description and controller.
Version-1 owning state for rate (kind 0) or jet (kind 1) controllers.
VolumeFluidPhase phase
Constitutive phase.
Definition VolumeFluid.h:64
float density
Rest density in kg/m3, in [0.1,100000].
Definition VolumeFluid.h:28
Owning world-space nozzle pose; rotation is a unit quaternion in XYZW order.
VolumeFluidMaterial material
Owned material parameters.
unsigned actorGroup
Fluid3D-compatible actor group in [0, 0x00ffffff]; equal groups use selfCollide instead of filters.
glm::vec4 orientation
Unit particle-shape orientation in XYZW order.
unsigned capacity
Maximum live particles; admission fails atomically at capacity.
Definition VolumeFluid.h:84
float spacing
Rest lattice separation in meters.
Definition VolumeFluid.h:86
Owning Fluid3DGranularEmitterBlueprint values used to prepare a native 3D emitter.
float randomness
Fluid3D radius-reduction percentage; runtime accepts the meaningful [0,100] range.
glm::vec4 color