1 #include "gui/objects/RenderJobs.h"
2 #include "gravity/BarnesHut.h"
3 #include "gravity/Moments.h"
4 #include "gui/Factory.h"
5 #include "gui/Project.h"
6 #include "gui/objects/Camera.h"
7 #include "gui/objects/Colorizer.h"
8 #include "gui/objects/Movie.h"
9 #include "run/IRun.h"
10 #include "run/VirtualSettings.h"
11 #include "run/jobs/IoJobs.h"
12 #include "system/Factory.h"
13 #include "system/Timer.h"
14
15 #ifdef SPH_USE_VDB
16 #include <openvdb/openvdb.h>
17 #endif
18
19
20 NAMESPACE_SPH_BEGIN
21
22 //-----------------------------------------------------------------------------------------------------------
23 // AnimationJob
24 //-----------------------------------------------------------------------------------------------------------
25
26 static RegisterEnum<AnimationType> sAnimation({
27 { AnimationType::SINGLE_FRAME, "single_frame", "Renders only single frame." },
28 { AnimationType::FILE_SEQUENCE, "file_sequence", "Make animation from saved files." },
29 });
30
31 enum class RenderColorizerId {
32 VELOCITY = int(ColorizerId::VELOCITY),
33 ENERGY = int(QuantityId::ENERGY),
34 DENSITY = int(QuantityId::DENSITY),
35 DAMAGE = int(QuantityId::DAMAGE),
36 GRAVITY = 666,
37 BEAUTY = int(ColorizerId::BEAUTY),
38 };
39
40 static RegisterEnum<RenderColorizerId> sColorizers({
41 { RenderColorizerId::VELOCITY, "velocity", "Particle velocities" },
42 { RenderColorizerId::ENERGY, "energy", "Specific internal energy" },
43 { RenderColorizerId::DENSITY, "density", "Density" },
44 { RenderColorizerId::DAMAGE, "damage", "Damage" },
45 { RenderColorizerId::GRAVITY, "gravity", "Gravitational acceleration" },
46 { RenderColorizerId::BEAUTY, "beauty", "Beauty" },
47 });
48
AnimationJob(const String & name)49 AnimationJob::AnimationJob(const String& name)
50 : IImageJob(name) {
51 animationType = EnumWrapper(AnimationType::SINGLE_FRAME);
52 colorizerId = EnumWrapper(RenderColorizerId::VELOCITY);
53 }
54
55 UnorderedMap<String, ExtJobType> AnimationJob::requires() const {
56 if (AnimationType(animationType) == AnimationType::FILE_SEQUENCE &&
57 RenderColorizerId(colorizerId) != RenderColorizerId::GRAVITY) {
58 return { { "camera", GuiJobType::CAMERA } };
59 } else {
60 return this->getSlots();
61 }
62 }
63
getSettings()64 VirtualSettings AnimationJob::getSettings() {
65 VirtualSettings connector;
66 addGenericCategory(connector, instName);
67
68 VirtualSettings::Category& outputCat = connector.addCategory("Output");
69 outputCat.connect("Directory", "directory", directory)
70 .setPathType(IVirtualEntry::PathType::DIRECTORY)
71 .setTooltip("Directory where the images are saved.");
72 outputCat.connect("File mask", "file_mask", fileMask)
73 .setTooltip(
74 "File mask of the created images. Can contain wildcard %d, which is replaced with the number of "
75 "the saved image");
76
77 auto particleEnabler = [this] {
78 return gui.get<RendererEnum>(GuiSettingsId::RENDERER) == RendererEnum::PARTICLE;
79 };
80 auto raymarcherEnabler = [this] {
81 return gui.get<RendererEnum>(GuiSettingsId::RENDERER) == RendererEnum::RAYMARCHER;
82 };
83 auto surfaceEnabler = [this] {
84 const RendererEnum type = gui.get<RendererEnum>(GuiSettingsId::RENDERER);
85 return type == RendererEnum::RAYMARCHER || type == RendererEnum::MESH;
86 };
87 auto volumeEnabler = [this] {
88 return gui.get<RendererEnum>(GuiSettingsId::RENDERER) == RendererEnum::VOLUME;
89 };
90
91 VirtualSettings::Category& rendererCat = connector.addCategory("Rendering");
92 rendererCat.connect<EnumWrapper>("Renderer", gui, GuiSettingsId::RENDERER);
93 rendererCat.connect("Quantity", "quantity", colorizerId);
94 rendererCat.connect("Include surface gravity", "surface_gravity", addSurfaceGravity)
95 .setEnabler([this] { return RenderColorizerId(colorizerId) == RenderColorizerId::GRAVITY; })
96 .setTooltip("Include the surface gravity of the particle itself.");
97 rendererCat.connect<bool>("Transparent background", "transparent", transparentBackground);
98 rendererCat.connect<EnumWrapper>("Color mapping", gui, GuiSettingsId::COLORMAP_TYPE);
99 rendererCat.connect<Float>("Logarithmic factor", gui, GuiSettingsId::COLORMAP_LOGARITHMIC_FACTOR)
100 .setEnabler(
101 [&] { return gui.get<ColorMapEnum>(GuiSettingsId::COLORMAP_TYPE) == ColorMapEnum::LOGARITHMIC; });
102 rendererCat.connect<Float>("Particle radius", gui, GuiSettingsId::PARTICLE_RADIUS)
103 .setEnabler(particleEnabler);
104 rendererCat.connect<bool>("Antialiasing", gui, GuiSettingsId::ANTIALIASED).setEnabler(particleEnabler);
105 rendererCat.connect<bool>("Show key", gui, GuiSettingsId::SHOW_KEY).setEnabler(particleEnabler);
106 rendererCat.connect<int>("Interation count", gui, GuiSettingsId::RAYTRACE_ITERATION_LIMIT)
107 .setEnabler([&] {
108 const RendererEnum type = gui.get<RendererEnum>(GuiSettingsId::RENDERER);
109 return type == RendererEnum::RAYMARCHER || type == RendererEnum::VOLUME;
110 });
111 rendererCat.connect<Float>("Surface level", gui, GuiSettingsId::SURFACE_LEVEL).setEnabler(surfaceEnabler);
112 rendererCat.connect<Vector>("Sun position", gui, GuiSettingsId::SURFACE_SUN_POSITION)
113 .setEnabler(surfaceEnabler);
114 rendererCat.connect<Float>("Sunlight intensity", gui, GuiSettingsId::SURFACE_SUN_INTENSITY)
115 .setEnabler(surfaceEnabler);
116 rendererCat.connect<Float>("Ambient intensity", gui, GuiSettingsId::SURFACE_AMBIENT)
117 .setEnabler(surfaceEnabler);
118 rendererCat.connect<Float>("Surface emission", gui, GuiSettingsId::SURFACE_EMISSION)
119 .setEnabler(raymarcherEnabler);
120 rendererCat.connect<EnumWrapper>("BRDF", gui, GuiSettingsId::RAYTRACE_BRDF).setEnabler(raymarcherEnabler);
121 rendererCat.connect<bool>("Render as spheres", gui, GuiSettingsId::RAYTRACE_SPHERES)
122 .setEnabler(raymarcherEnabler);
123 rendererCat.connect<bool>("Enable shadows", gui, GuiSettingsId::RAYTRACE_SHADOWS)
124 .setEnabler(raymarcherEnabler);
125 rendererCat.connect<Float>("Medium emission [km^-1]", gui, GuiSettingsId::VOLUME_EMISSION)
126 .setUnits(1.e-3_f)
127 .setEnabler(volumeEnabler);
128 rendererCat.connect<Float>("Medium absorption [km^-1]", gui, GuiSettingsId::VOLUME_ABSORPTION)
129 .setUnits(1.e-3_f)
130 .setEnabler(volumeEnabler);
131 rendererCat.connect<bool>("Reduce noise", gui, GuiSettingsId::REDUCE_LOWFREQUENCY_NOISE)
132 .setEnabler(volumeEnabler);
133 rendererCat.connect<Float>("Bloom intensity", gui, GuiSettingsId::BLOOM_INTENSITY)
134 .setEnabler(volumeEnabler);
135
136 VirtualSettings::Category& textureCat = connector.addCategory("Texture paths");
137 textureCat.connect<Path>("Background", gui, GuiSettingsId::RAYTRACE_HDRI)
138 .setEnabler([this] {
139 const RendererEnum id = gui.get<RendererEnum>(GuiSettingsId::RENDERER);
140 return id == RendererEnum::VOLUME || id == RendererEnum::RAYMARCHER;
141 })
142 .setPathType(IVirtualEntry::PathType::INPUT_FILE);
143
144 auto sequenceEnabler = [this] { return AnimationType(animationType) == AnimationType::FILE_SEQUENCE; };
145
146 VirtualSettings::Category& animationCat = connector.addCategory("Animation");
147 animationCat.connect<EnumWrapper>("Animation type", "animation_type", animationType);
148 animationCat.connect<Path>("First file", "first_file", sequence.firstFile)
149 .setPathType(IVirtualEntry::PathType::INPUT_FILE)
150 .setFileFormats(getInputFormats())
151 .setEnabler(sequenceEnabler);
152 animationCat.connect("Interpolated frames", "extra_frames", extraFrames)
153 .setEnabler(sequenceEnabler)
154 .setTooltip("Sets the number of extra frames added between each two state files.");
155
156 return connector;
157 }
158
159 class GravityColorizer : public TypedColorizer<Float> {
160 private:
161 SharedPtr<IScheduler> scheduler;
162 BarnesHut gravity;
163 Array<Float> acc;
164 Float G;
165 bool addSurfaceGravity;
166
167 public:
GravityColorizer(const SharedPtr<IScheduler> & scheduler,const Palette & palette,const Float G,const bool addSurfaceGravity)168 GravityColorizer(const SharedPtr<IScheduler>& scheduler,
169 const Palette& palette,
170 const Float G,
171 const bool addSurfaceGravity)
172 : TypedColorizer<Float>(QuantityId::POSITION, std::move(palette))
173 , scheduler(scheduler)
174 , gravity(0.8_f, MultipoleOrder::OCTUPOLE, 25, 50, G)
175 , G(G)
176 , addSurfaceGravity(addSurfaceGravity) {}
177
initialize(const Storage & storage,const RefEnum UNUSED (ref))178 virtual void initialize(const Storage& storage, const RefEnum UNUSED(ref)) override {
179 acc.resize(storage.getParticleCnt());
180 acc.fill(0._f);
181
182 // gravitation acceleration from other particles
183 gravity.build(*scheduler, storage);
184
185 Array<Vector> dv(storage.getParticleCnt());
186 dv.fill(Vector(0._f));
187 Statistics stats;
188 gravity.evalSelfGravity(*scheduler, dv, stats);
189 for (Size i = 0; i < dv.size(); ++i) {
190 acc[i] = getLength(dv[i]);
191 }
192
193 if (addSurfaceGravity) {
194 // add surface gravity of each particle
195 ArrayView<const Float> m = storage.getValue<Float>(QuantityId::MASS);
196 ArrayView<const Vector> r = storage.getValue<Vector>(QuantityId::POSITION);
197 for (Size i = 0; i < r.size(); ++i) {
198 acc[i] += G * m[i] / sqr(r[i][H]);
199 }
200 }
201 }
202
isInitialized() const203 virtual bool isInitialized() const override {
204 return !acc.empty();
205 }
206
evalColor(const Size idx) const207 virtual Rgba evalColor(const Size idx) const override {
208 return palette(acc[idx]);
209 }
210
evalVector(const Size UNUSED (idx)) const211 virtual Optional<Vector> evalVector(const Size UNUSED(idx)) const override {
212 return NOTHING;
213 }
214
name() const215 virtual String name() const override {
216 // needs to 'pretend' to be acceleration to work with palette accessor in IR
217 return "Acceleration";
218 }
219 };
220
getRenderParams(const GuiSettings & gui) const221 RenderParams AnimationJob::getRenderParams(const GuiSettings& gui) const {
222 SharedPtr<CameraData> camera = getInput<CameraData>("camera");
223 RenderParams params;
224 params.camera = camera->camera->clone();
225 params.tracker = std::move(camera->tracker);
226 GuiSettings paramGui = gui;
227 paramGui.addEntries(camera->overrides);
228 params.initialize(paramGui);
229 return params;
230 }
231
232 class AnimationRenderOutput : public IRenderOutput {
233 private:
234 IRunCallbacks& callbacks;
235 IRenderer& renderer;
236 Size iterationCnt;
237
238 Timer timer;
239 Size iteration = 0;
240
241 public:
AnimationRenderOutput(IRunCallbacks & callbacks,IRenderer & renderer,const Size iterationCnt)242 AnimationRenderOutput(IRunCallbacks& callbacks, IRenderer& renderer, const Size iterationCnt)
243 : callbacks(callbacks)
244 , renderer(renderer)
245 , iterationCnt(iterationCnt) {}
246
update(const Bitmap<Rgba> & bitmap,Array<Label> && labels,const bool isFinal)247 virtual void update(const Bitmap<Rgba>& bitmap, Array<Label>&& labels, const bool isFinal) override {
248 this->update(bitmap.clone(), std::move(labels), isFinal);
249 }
250
update(Bitmap<Rgba> && bitmap,Array<Label> && labels,const bool UNUSED (isFinal))251 virtual void update(Bitmap<Rgba>&& bitmap, Array<Label>&& labels, const bool UNUSED(isFinal)) override {
252 SharedPtr<AnimationFrame> frame = makeShared<AnimationFrame>();
253 frame->bitmap = std::move(bitmap);
254 frame->labels = std::move(labels);
255 Storage storage;
256 storage.setUserData(frame);
257
258 Statistics stats;
259 stats.set(StatisticsId::RELATIVE_PROGRESS, Float(++iteration) / iterationCnt);
260 stats.set(StatisticsId::WALLCLOCK_TIME, int(timer.elapsed(TimerUnit::MILLISECOND)));
261 callbacks.onTimeStep(storage, stats);
262
263 if (callbacks.shouldAbortRun()) {
264 renderer.cancelRender();
265 }
266 }
267 };
268
evaluate(const RunSettings & global,IRunCallbacks & callbacks)269 void AnimationJob::evaluate(const RunSettings& global, IRunCallbacks& callbacks) {
270 /// \todo maybe also work with a copy of Gui ?
271 gui.set(GuiSettingsId::BACKGROUND_COLOR, Rgba(0.f, 0.f, 0.f, transparentBackground ? 0.f : 1.f));
272 gui.set(GuiSettingsId::RAYTRACE_SUBSAMPLING, 0);
273 int iterLimit = 1;
274 if (gui.get<RendererEnum>(GuiSettingsId::RENDERER) != RendererEnum::PARTICLE) {
275 iterLimit = gui.get<int>(GuiSettingsId::RAYTRACE_ITERATION_LIMIT);
276 }
277
278 SharedPtr<IScheduler> scheduler = Factory::getScheduler(global);
279 AutoPtr<IRenderer> renderer = Factory::getRenderer(scheduler, gui);
280 RawPtr<IRenderer> rendererPtr = renderer.get();
281
282 RenderParams params = this->getRenderParams(gui);
283 AutoPtr<IColorizer> colorizer = this->getColorizer(global);
284
285 int firstIndex = 0;
286 if (AnimationType(animationType) == AnimationType::FILE_SEQUENCE) {
287 Optional<Size> sequenceFirstIndex = OutputFile::getDumpIdx(sequence.firstFile);
288 if (sequenceFirstIndex) {
289 firstIndex = sequenceFirstIndex.value();
290 }
291 }
292 OutputFile paths(directory / Path(fileMask), firstIndex);
293 Movie movie(gui, std::move(renderer), std::move(colorizer), std::move(params), extraFrames, paths);
294
295 switch (AnimationType(animationType)) {
296 case AnimationType::SINGLE_FRAME: {
297 SharedPtr<ParticleData> data = this->getInput<ParticleData>("particles");
298 AnimationRenderOutput output(callbacks, *rendererPtr, iterLimit);
299 movie.render(std::move(data->storage), std::move(data->stats), output);
300 break;
301 }
302 case AnimationType::FILE_SEQUENCE: {
303 FlatMap<Size, Path> fileMap = getFileSequence(sequence.firstFile);
304 if (fileMap.empty()) {
305 throw InvalidSetup("No files to render.");
306 }
307
308 const Size iterationCnt = iterLimit * fileMap.size() * (extraFrames + 1);
309 AnimationRenderOutput output(callbacks, *rendererPtr, iterationCnt);
310 AutoPtr<IInput> input = Factory::getInput(sequence.firstFile);
311 for (auto& element : fileMap) {
312 Storage frame;
313 Statistics stats;
314 const Outcome result = input->load(element.value(), frame, stats);
315 if (!result) {
316 /// \todo how to report this? (don't do modal dialog)
317 }
318
319 if (callbacks.shouldAbortRun()) {
320 break;
321 }
322
323 movie.render(std::move(frame), std::move(stats), output);
324 }
325 break;
326 }
327 default:
328 NOT_IMPLEMENTED;
329 }
330 }
331
332 class RenderPreview : public IRenderPreview {
333 private:
334 RenderParams params;
335 AutoPtr<IRenderer> renderer;
336 AutoPtr<IColorizer> colorizer;
337 SharedPtr<ParticleData> data;
338 std::atomic_bool cancelled;
339
340 bool rendererDirty = true;
341 bool colorizerDirty = true;
342
343 public:
RenderPreview(RenderParams && params,AutoPtr<IRenderer> && renderer,AutoPtr<IColorizer> && colorizer,const SharedPtr<ParticleData> & data)344 RenderPreview(RenderParams&& params,
345 AutoPtr<IRenderer>&& renderer,
346 AutoPtr<IColorizer>&& colorizer,
347 const SharedPtr<ParticleData>& data)
348 : params(std::move(params))
349 , renderer(std::move(renderer))
350 , colorizer(std::move(colorizer))
351 , data(data)
352 , cancelled(false) {}
353
render(const Pixel resolution,IRenderOutput & output)354 virtual void render(const Pixel resolution, IRenderOutput& output) override {
355 cancelled = false;
356
357 // lazy init
358 if (colorizerDirty) {
359 colorizer->initialize(data->storage, RefEnum::WEAK);
360 colorizerDirty = false;
361 rendererDirty = true;
362 }
363 if (cancelled) {
364 return;
365 }
366 if (rendererDirty) {
367 renderer->initialize(data->storage, *colorizer, *params.camera);
368 rendererDirty = false;
369 }
370 if (cancelled) {
371 return;
372 }
373
374 Pixel size = params.camera->getSize();
375 size = correctAspectRatio(resolution, float(size.x) / float(size.y));
376 params.camera->resize(size);
377 Statistics dummy;
378 renderer->render(params, dummy, output);
379 }
380
update(RenderParams && newParams)381 virtual void update(RenderParams&& newParams) override {
382 AutoPtr<ICamera> camera = std::move(params.camera);
383 params = std::move(newParams);
384 params.camera = std::move(camera);
385 }
386
update(AutoPtr<ICamera> && newCamera)387 virtual void update(AutoPtr<ICamera>&& newCamera) override {
388 params.camera = std::move(newCamera);
389 }
390
update(AutoPtr<IColorizer> && newColorizer)391 virtual void update(AutoPtr<IColorizer>&& newColorizer) override {
392 colorizer = std::move(newColorizer);
393 colorizerDirty = true;
394 }
395
update(AutoPtr<IRenderer> && newRenderer)396 virtual void update(AutoPtr<IRenderer>&& newRenderer) override {
397 renderer = std::move(newRenderer);
398 rendererDirty = true;
399 }
400
update(Palette && palette)401 virtual void update(Palette&& palette) override {
402 colorizer->setPalette(palette);
403 renderer->setColorizer(*colorizer);
404 }
405
cancel()406 virtual void cancel() override {
407 cancelled = true;
408 renderer->cancelRender();
409 }
410
411 private:
correctAspectRatio(const Pixel resolution,const float aspect) const412 Pixel correctAspectRatio(const Pixel resolution, const float aspect) const {
413 const float current = float(resolution.x) / float(resolution.y);
414 if (current > aspect) {
415 return Pixel(resolution.x * aspect / current, resolution.y);
416 } else {
417 return Pixel(resolution.x, resolution.y * current / aspect);
418 }
419 }
420 };
421
getRenderPreview(const RunSettings & global) const422 AutoPtr<IRenderPreview> AnimationJob::getRenderPreview(const RunSettings& global) const {
423 if (AnimationType(animationType) != AnimationType::SINGLE_FRAME) {
424 throw InvalidSetup("Only enabled for single-frame renders");
425 }
426
427 if (!inputs.contains("particles")) {
428 throw InvalidSetup("Particles not connected");
429 }
430
431 RenderParams params = this->getRenderParams();
432
433 AutoPtr<IColorizer> colorizer = this->getColorizer(global);
434 if (!colorizer) {
435 throw InvalidSetup("No quantity selected");
436 }
437 AutoPtr<IRenderer> renderer = this->getRenderer(global);
438
439 SharedPtr<ParticleData> data = this->getInput<ParticleData>("particles");
440
441 return makeAuto<RenderPreview>(std::move(params), std::move(renderer), std::move(colorizer), data);
442 }
443
getColorizer(const RunSettings & global) const444 AutoPtr<IColorizer> AnimationJob::getColorizer(const RunSettings& global) const {
445 Project project = Project::getInstance().clone();
446 project.getGuiSettings() = gui;
447 RenderColorizerId renderId(colorizerId);
448 if (renderId == RenderColorizerId::GRAVITY) {
449 Palette palette;
450 if (!project.getPalette("Acceleration", palette)) {
451 palette = Factory::getPalette(ColorizerId::ACCELERATION);
452 }
453 SharedPtr<IScheduler> scheduler = Factory::getScheduler(global);
454 SharedPtr<ParticleData> data = this->getInput<ParticleData>("particles");
455 Float G = Constants::gravity;
456 if (data->overrides.has(RunSettingsId::GRAVITY_CONSTANT)) {
457 G = data->overrides.get<Float>(RunSettingsId::GRAVITY_CONSTANT);
458 }
459 return makeAuto<GravityColorizer>(scheduler, palette, G, addSurfaceGravity);
460 } else {
461 return Factory::getColorizer(project, ColorizerId(renderId));
462 }
463 }
464
getRenderer(const RunSettings & global) const465 AutoPtr<IRenderer> AnimationJob::getRenderer(const RunSettings& global) const {
466 SharedPtr<IScheduler> scheduler = Factory::getScheduler(global);
467 GuiSettings previewGui = gui;
468 previewGui.set(GuiSettingsId::RAYTRACE_SUBSAMPLING, 4);
469 previewGui.set(GuiSettingsId::BACKGROUND_COLOR, Rgba(0.f, 0.f, 0.f, transparentBackground ? 0.f : 1.f));
470 AutoPtr<IRenderer> renderer = Factory::getRenderer(scheduler, previewGui);
471 return renderer;
472 }
473
getRenderParams() const474 RenderParams AnimationJob::getRenderParams() const {
475 GuiSettings previewGui = gui;
476 previewGui.set(GuiSettingsId::SHOW_KEY, false);
477 previewGui.set(GuiSettingsId::BACKGROUND_COLOR, Rgba(0.f, 0.f, 0.f, transparentBackground ? 0.f : 1.f));
478 return this->getRenderParams(previewGui);
479 }
480
481 JobRegistrar sRegisterAnimation(
482 "render animation",
483 "animation",
484 "rendering",
__anon57ff93230a02(const String& name) 485 [](const String& name) { return makeAuto<AnimationJob>(name); },
486 "Renders an image or a sequence of images from given particle input(s)");
487
488 //-----------------------------------------------------------------------------------------------------------
489 // VdbJob
490 //-----------------------------------------------------------------------------------------------------------
491
492 #ifdef SPH_USE_VDB
493
494
vectorToVec3f(const Vector & v)495 INLINE openvdb::Vec3f vectorToVec3f(const Vector& v) {
496 return openvdb::Vec3f(v[X], v[Y], v[Z]);
497 }
498
worldToRelative(const Vector & r,const Box & box,const Indices & dims)499 INLINE Vector worldToRelative(const Vector& r, const Box& box, const Indices& dims) {
500 return (r - box.lower()) / box.size() * Vector(dims);
501 }
502
relativeToWorld(const Vector & r,const Box & box,const Indices & dims)503 INLINE Vector relativeToWorld(const Vector& r, const Box& box, const Indices& dims) {
504 return r * box.size() / Vector(dims) + box.lower();
505 }
506
getParticleBox(const Vector & r,const Box & box,const Indices & dims)507 Tuple<Indices, Indices> getParticleBox(const Vector& r, const Box& box, const Indices& dims) {
508 const Vector from = worldToRelative(r - Vector(2._f * r[H]), box, dims);
509 const Vector to = worldToRelative(r + Vector(2._f * r[H]), box, dims);
510 const Indices fromIdxs(ceil(from[X]), ceil(from[Y]), ceil(from[Z]));
511 const Indices toIdxs(floor(to[X]), floor(to[Y]), floor(to[Z]));
512 return { max(fromIdxs, Indices(0._f)), min(toIdxs, dims - Indices(1)) };
513 }
514
getSettings()515 VirtualSettings VdbJob::getSettings() {
516 VirtualSettings connector;
517 addGenericCategory(connector, instName);
518
519 VirtualSettings::Category& gridCat = connector.addCategory("Grid parameters");
520 gridCat.connect("Grid start [km]", "grid_start", gridStart)
521 .setUnits(1.e3_f)
522 .setTooltip("Sets the lower bound of the bounding box.");
523 gridCat.connect("Grid end [km]", "grid_end", gridEnd)
524 .setUnits(1.e3_f)
525 .setTooltip("Sets the upper bound of the bounding box.");
526 gridCat.connect("Resolution power", "power", dimPower)
527 .setTooltip("Defines resolution of the grid. The number of voxels in one dimension is 2^power.");
528 gridCat.connect("Surface level", "surface_level", surfaceLevel).setTooltip("Iso-value of the surface.");
529
530 VirtualSettings::Category& inputCat = connector.addCategory("File sequence");
531 inputCat.connect("Enable", "enable_sequence", sequence.enabled);
532 inputCat.connect("First file", "first_file", sequence.firstFile)
533 .setPathType(IVirtualEntry::PathType::INPUT_FILE)
534 .setFileFormats(getInputFormats())
535 .setEnabler([this] { return sequence.enabled; });
536
537 VirtualSettings::Category& outputCat = connector.addCategory("Output");
538 outputCat.connect("VDB File", "file", path)
539 .setPathType(IVirtualEntry::PathType::OUTPUT_FILE)
540 .setFileFormats({ { "OpenVDB grid file", "vdb" } })
541 .setEnabler([this] { return !sequence.enabled; });
542
543 return connector;
544 }
545
evaluate(const RunSettings & global,IRunCallbacks & callbacks)546 void VdbJob::evaluate(const RunSettings& global, IRunCallbacks& callbacks) {
547 openvdb::initialize();
548 auto deinit = finally([] { openvdb::uninitialize(); });
549
550 if (sequence.enabled) {
551 FlatMap<Size, Path> fileMap = getFileSequence(sequence.firstFile);
552 if (fileMap.empty()) {
553 throw InvalidSetup("No files to render.");
554 }
555 const Size firstKey = fileMap.begin()->key();
556
557 AutoPtr<IInput> input = Factory::getInput(sequence.firstFile);
558 for (auto& element : fileMap) {
559 Storage storage;
560 Statistics stats;
561 const Outcome result = input->load(element.value(), storage, stats);
562 if (!result) {
563 /// \todo how to report this? (don't do modal dialog)
564 }
565
566 Path outputPath = element.value();
567 outputPath.replaceExtension("vdb");
568 this->generate(storage, global, outputPath);
569
570 /// \todo deduplicate with AnimationJob
571 stats.set(StatisticsId::RELATIVE_PROGRESS, Float(element.key() - firstKey) / fileMap.size());
572 if (element.key() == firstKey) {
573 callbacks.onSetUp(storage, stats);
574 }
575 callbacks.onTimeStep(storage, stats);
576
577 if (callbacks.shouldAbortRun()) {
578 break;
579 }
580 }
581 } else {
582 Storage& storage = getInput<ParticleData>("particles")->storage;
583 this->generate(storage, global, path);
584 }
585 }
586
generate(Storage & storage,const RunSettings & global,const Path & outputPath)587 void VdbJob::generate(Storage& storage, const RunSettings& global, const Path& outputPath) {
588 using namespace openvdb;
589
590 FloatGrid::Ptr colorField = FloatGrid::create(-surfaceLevel);
591 Vec3SGrid::Ptr velocityField = Vec3SGrid::create(vectorToVec3f(Vector(0._f)));
592 FloatGrid::Ptr energyField = FloatGrid::create(0._f);
593
594 colorField->setName("Density");
595 velocityField->setName("Velocity");
596 energyField->setName("Emission");
597
598 ArrayView<const Vector> r = storage.getValue<Vector>(QuantityId::POSITION);
599 ArrayView<const Vector> v = storage.getDt<Vector>(QuantityId::POSITION);
600 ArrayView<const Float> m = storage.getValue<Float>(QuantityId::MASS);
601 ArrayView<const Float> u = storage.getValue<Float>(QuantityId::ENERGY);
602 ArrayView<const Float> rho = storage.getValue<Float>(QuantityId::DENSITY);
603
604 const Box box(gridStart, gridEnd);
605 const Size gridSize = 1 << dimPower;
606 const Indices gridIdxs(gridSize);
607
608 LutKernel<3> kernel = Factory::getKernel<3>(global);
609
610 typename FloatGrid::Accessor colorAccessor = colorField->getAccessor();
611 typename Vec3SGrid::Accessor velocityAccessor = velocityField->getAccessor();
612 typename FloatGrid::Accessor energyAccessor = energyField->getAccessor();
613 for (Size i = 0; i < r.size(); ++i) {
614 Indices from, to;
615 tieToTuple(from, to) = getParticleBox(r[i], box, gridIdxs);
616 Float rho_i;
617 if (storage.getMaterialCnt() > 0) {
618 rho_i = storage.getMaterialOfParticle(i)->getParam<Float>(BodySettingsId::DENSITY);
619 } else {
620 rho_i = rho[i];
621 }
622 for (int x = from[X]; x <= to[X]; ++x) {
623 for (int y = from[Y]; y <= to[Y]; ++y) {
624 for (int z = from[Z]; z <= to[Z]; ++z) {
625 const Indices idxs(x, y, z);
626 const Vector pos = relativeToWorld(idxs, box, gridIdxs);
627 const Float w = kernel.value(r[i] - pos, r[i][H]);
628 const Float c = m[i] / rho_i * w;
629
630 const Coord coord(x, y, z);
631 colorAccessor.modifyValue(coord, [c](float& color) { color += c; });
632 energyAccessor.modifyValue(coord, [&u, c, i](float& energy) { energy += c * u[i]; });
633 velocityAccessor.modifyValue(
634 coord, [&v, c, i](Vec3f& velocity) { velocity += c * vectorToVec3f(v[i]); });
635 }
636 }
637 }
638 }
639
640 for (FloatGrid::ValueOnIter iter = colorField->beginValueOn(); iter; ++iter) {
641 const Coord coord = iter.getCoord();
642 const float c = *iter;
643 if (c > 0) {
644 energyAccessor.modifyValue(coord, [c](float& energy) { energy /= c; });
645 velocityAccessor.modifyValue(coord, [c](Vec3f& velocity) { velocity /= c; });
646 }
647 iter.setValue(c - surfaceLevel);
648 }
649
650 GridPtrVec vdbGrids;
651 vdbGrids.push_back(colorField);
652 vdbGrids.push_back(velocityField);
653 vdbGrids.push_back(energyField);
654
655 Path vdbPath = outputPath;
656 vdbPath.replaceExtension("vdb");
657 io::File vdbFile(vdbPath.native().cstr());
658 vdbFile.write(vdbGrids);
659 vdbFile.close();
660 }
661
662 JobRegistrar sRegisterVdb(
663 "save VDB grid",
664 "grid",
665 "rendering",
__anon57ff93231302(const String& name) 666 [](const String& name) { return makeAuto<VdbJob>(name); },
667 "Converts the particle data into a volumetric grid in OpenVDB format.");
668
669 #endif
670
671 NAMESPACE_SPH_END
672