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