ManyObjects
The signed distance to a scene of many objects is the smallest signed distance to any of them. Computed literally, every query visits every object, so the cost grows with the number of objects. This example builds two such scenes, 8,000 spheres on a lattice and a “city” of 3,600 randomly sized boxes, and evaluates each in two ways: a naive loop over every object, and a BVHUnion, which evaluates the same union through a bounding volume hierarchy over the objects’ boxes (see Bounding volume hierarchies). It times both. Its last section runs the unions on a GPU and shows how to time the queries there without timing the memory traffic around them.
The source is at Examples/ManyObjects/main.cpp. See Building for how to compile
it with CMake, GNU Make, or a direct compiler invocation.
cd Examples/ManyObjects
./ManyObjects.ex
Fig. 29 The random city, a BVH union of boxes.
The query
The query is a small functor whose EBGEOMETRY_HOST_DEVICE call operator returns the union’s
signed distance at a point. It holds the union by value. A BVHUnion is a descriptor of the
objects and the BVH it stored in a Pool, so the copy
is cheap, and a kernel can receive it once it describes a pool the device can read.
Building the scenes
A BVHUnion holds one primitive type, here
SphereSDF for one scene and
BoxSDF for the other. Its constructor takes the
pool, the objects and one bounding box per object:
EBGeometry::Pool pool(EBGeometry::hostMemoryResource());
const EBGeometry::BVHUnion<T, Box, K> city(pool, buildings, buildingBoxes);
The union copies the objects into the pool and builds its BVH there, so the input vectors are not needed afterwards. The pool must outlive the union and every copy of it. To combine objects of different types in one union, compose them with the expression builder instead (Basic CSG).
Using the unions on the host
signedDistance(point) gives the same value as the naive minimum over every object: the BVH only
skips objects whose boxes show that they cannot be the closest. For each scene the program prints the
time per query point of both, and the speedup, which grows with the number of objects. Change
spheresPerAxis, lotsPerAxis and numPoints at the top of the file to see this.
computeBoundingVolume() gives the box around the whole union, which places the random query
points.
Timing the queries on a GPU
The pool holding both unions is frozen, mirrored into GPU::memoryResource() (device memory in a
CUDA or HIP build) and each union rebased onto the copy with rebasedView(), as in
MeshSDF.
The one-call
GPU::transform(query, points, results) takes
the points in a host std::vector and writes the results into another. In a GPU build, every call allocates
device memory, copies the points in, runs the kernel, copies the results out and frees the memory, so
timing it times all of that. To time the queries alone, the example puts the points, and room for the
results, on the device first. Both are
PODVectors in a pool of their own, mirrored
once:
in.reserveFrom(hostPool, n);
out.reserveFrom(hostPool, n);
in.assign(hostPool.base(), points.data(), n);
out.assign(hostPool.base(), zeros.data(), n);
hostPool.freeze();
const Pool devicePool = Pool::mirror(hostPool, GPU::memoryResource());
GPU::transform(query, devicePool, in, out);
A PODVector is an offset and a size, so the pool overload of GPU::transform takes the pool
both arrays live in. It checks that the pool is frozen and in memory the device can reach, then
reads and writes the arrays where they are, with no allocation and no copies. Mirroring
devicePool back to host memory then makes the results readable. (The span overload, over
PODSpans from in.bind(devicePool.base()),
does the same without the check.)
The program prints both times per point. On a GPU the gap between them is the copying. In a plain
C++ build the section runs on the host, where GPU::memoryResource() is host memory standing in
for a device and GPU::transform is a loop, so the two times are about the same. In a GPU build
the section uses about a million points, so that the time is the kernel’s rather than the launch’s;
on the host it keeps numPoints. See Running on a GPU for building for a GPU.
Looking at it
The example ends with a window on the random city. Its buildings rise along \(z\), so it fits the camera with \(z\) up, and the floor goes under them:
const auto camera = Render::Camera<T>::fit(city.computeBoundingVolume(), Vec3(0, 0, 1));
Render::view(city, pool, camera, Render::Settings<T>(), argc, argv);
The window needs SDL2 and is built with make VIEWER=SDL2 or cmake -DEBGEOMETRY_VIEWER=SDL2 (see
The viewer window (EBGEOMETRY_VIEWER)); otherwise, and with --no-view or without a display, view() prints why
and returns. Drag to orbit and scroll to zoom; the window lists its other keys (see
The viewer’s keys).