The renderer and the viewer

The renderer is in EBGeometry_Render.hpp and the window in EBGeometry_Viewer.hpp; EBGeometry.hpp includes both. Every ray it casts is a firstHit() on the shape (see Ray queries), so it renders any shape, exactly where the shape has a ray form. Rendering describes the method.

The renderer

The classes are in namespace Render:

Render::Settings<T> settings;                       // 960 x 960, 2 x 2 rays per pixel, ...
settings.m_colorBy = Render::ColorBy::Primitive;

const Render::Renderer<T> renderer(settings);
const auto                camera = Render::Camera<T>::fit(shape.computeBoundingVolume());

Render::Image       image;                          // 0xRRGGBBAA, row by row from the top
const Render::Stats stats = renderer.render(shape, camera, image);
  • Camera orbits a target: yaw about the up axis (+y by default), pitch above the horizontal plane, a distance and a vertical field of view. Camera::fit(box) aims it at a box from far enough to see all of it, and Camera::fit(box, view) does so from a named direction (View::Front, Back, Left, Right, Top or Bottom; the pitch may be 90 degrees, straight along the up axis); Camera::lookAt(eye, target) places it directly. m_orthographic makes the rays parallel, the view as tall as the perspective view at the target, and m_nearClip starts every camera ray that far from the eye. A shape with an unbounded box (a plane, a complement) has nothing to fit; give it a camera, or fit the camera to a box of your own.

  • Settings is a plain value: the image size and rays per pixel, the host threads, shadows, ambient occlusion, the floor (with or without its checkerboard, and the checker’s size) and the sky (its horizon and zenith colors, or a plain background), how to color (ColorBy::Normal, the default, ColorBy::Uniform or ColorBy::Primitive), the colors, the light (its direction, by default in the camera’s frame so that it turns with the camera and lights the side in view, or in the scene with m_lightFromCamera false; its intensity, the ambient level, and the highlights’ strength and shininess), the section plane (m_section, a point and a normal; the side the normal points to is removed; m_isoSpacing draws lines of equal distance on it; the cut face is colored as a surface facing along the plane’s normal would be, and partly transparent, m_cutOpacity, so that the shape’s next surface behind it shows through; its contour, a line m_contourWidth pixels wide, and a sheet of opacity m_sheetOpacity outside the shape within its box), and the march (RayOptions). m_primitiveColors gives the first Settings::PrimitiveColors (16) primitive ids colors of their own; an entry left unset, and every later id, takes the built-in palette’s (Materials colors its bore steel gray). m_highlightPrimitive tints one primitive’s surface in m_highlightColor. Render::writeImage(image, file) writes an image as binary PPM. Render::toCode(camera, settings) writes a camera and settings as C++ statements, the fields that differ from their defaults.

  • Renderer holds the settings. render(shape, camera, image) runs on the host’s threads (Settings::m_threads, by default one per hardware thread); renderOnDevice(deviceShape, camera, image) runs the same pixel functor through GPU::parallelFor() on a device view of the shape, as described in Running queries on a GPU. Overloads of both take a std::atomic<bool> that another thread sets to stop the render early (between rows on the host, between bands of rows on a device), and a counter of the rows done; a stopped render says so in Stats::m_complete. pick(shape, camera, column, row) returns the hit (HitT: the primitive, the point, the normal) of the ray through a point of the image, cut as the render cuts it, so that it names what a render draws there. Each pixel is PixelQuery, a trivially copyable functor that reads everything it needs about the shape (its box, its scale, where the floor goes) from the shape itself.

  • Every shape is shaded the same way, through firstHit() (shadows, ambient occlusion and the section plane’s cut included), so the renderer needs nothing of a shape beyond what makes it a shape.

  • HitT::m_primitive colors by primitive: a tape’s leaf or a mesh SDF’s face; any other shape (a fused composition, an analytic shape) is one primitive, 0 (see Ray queries).

  • Stats reports the rays cast, those that hit the shape, those that hit it from inside the solid (drawn tinted red), those that were unresolved (drawn as misses), and whether the eye is inside the solid. The renderer prints nothing.

The viewer

Render::view(shape, argc, argv) opens a window on a shape, from a camera fitted to its box; overloads take a camera and settings, and a device view of the shape to render on a GPU. Render::view(shape, pool, argc, argv) takes instead the pool the shape lives in: in a GPU build with a visible device it freezes the pool, mirrors it for as long as the window is open and renders through the shape’s rebasedView() of the copy, and otherwise renders on the host. The pool is frozen before the window opens, so it stays frozen even if SDL then cannot open one.

While the camera moves the window shows a quick preview (one ray per pixel, no shadows, at a resolution it adapts to keep each preview at a few tens of milliseconds). Once the camera rests it refines on a background thread – full resolution without shadows, then the full settings – and moving the camera again cancels the refinement at once, so the window always answers. The window can be resized, and renders at its size in pixels.

An overlay in the top left corner shows the frame (its size and time, the hits), then each group of keys below with the state it changes; H hides it. The axes are drawn in the bottom left corner. The keys are also printed, in the same groups, when the window opens.

A click (the left button, without dragging) picks the object under the cursor: a tape’s leaf (so that a drilled solid’s outer surface and its hole are picked apart), a member of a BVH union (not a smooth one), or a mesh SDF as a whole (primitivesAreFaces), whose face is named too; any other shape, a fused composition among them, is picked as a whole. The object is tinted gold (Settings::m_highlightPrimitive, AllPrimitives for a whole mesh) and outlined, from the object each pixel’s first ray hit (Image::m_primitives, filled when Settings::m_recordPrimitives asks, as the viewer does), and its id, point and normal are printed; a click on the background clears it. With a section plane on, the overlay shows the point under the cursor on the plane and the shape’s value there; Shift and a click on two surfaces measures the distance between them.

Table 3 The viewer’s keys

Key

Action

Drag, Ctrl + drag, wheel

Orbit the camera, pan it, zoom.

1, 2, 3; O; R

Look from the front, the side, the top; switch between perspective and orthographic; reset the camera.

+, -; double-click

A narrower or wider field of view; center the camera on the point under the cursor.

Right drag

Move the light.

Left, Right (Shift + Left, Right)

Weaker or stronger highlights, m_specular (broader or tighter, m_shininess).

P

Cycle the coloring: by normal, uniform, by primitive.

S, A, F, B

Shadows, ambient occlusion, the floor, the sky on or off.

Shift + Up, Down

More or fewer rays per pixel, from 1 x 1 to 8 x 8.

G

Render on the GPU or the CPU (with a device view).

X; Ctrl + X; N

Cut with a section plane across x, then y, then z, through the box’s center (a plane the settings turn on comes first); remove the cut; reverse it (remove the other side).

Ctrl + wheel; Up, Down

Move the section plane in fine steps; in coarse ones.

I

Lines of equal distance on the section plane: off, as the settings set them (if they do), every 1/20 of the box’s diagonal, every 1/50.

Click; Shift + click

Pick the object under the cursor; mark a point to measure from or to (the second shows the distance and a line between them). On the background, either clears.

C; W

Print the camera and settings as C++ (toCode()), to paste a view found by hand into a program; save the full frame as a PPM image (writeImage()) in the working directory.

H; Esc, Q

Show or hide the overlay and the axes; close the window.

The window needs SDL2 and is compiled only on request (see The viewer window (EBGEOMETRY_VIEWER)). Without it, and whenever no window should open – the program was given --no-view, there is no display, or SDL cannot start – view() prints one line saying why and returns, so a program that calls it still runs unattended (ctest passes --no-view to the examples that call it).

The MeshSDF example ends by opening the viewer on the mesh it read.