Feature: Motion Blur

External libraries:
No further libraries than nori-framework has been used to complete this task.
Time spent:
20:00 (approximately)
Acknowledgements:
  • -
Files:
  • include/nori/motion.h (new)
  • src/motion.cpp (new)
  • src/motion_direct_mis.cpp (new)
  • src/motion_path_mis.cpp (new)
  • include/nori/camera.h (updated)
  • include/nori/common.h (updated)
  • include/nori/object.h (updated)
  • include/nori/ray.h (updated)
  • include/nori/scene.h (updated)
  • include/nori/shape.h (updated)
  • src/common.cpp (updated)
  • src/cube.cpp (updated)
  • src/mesh.cpp (updated)
  • src/parser.cpp (updated)
  • src/perspective.cpp (updated)
  • src/renderer.cpp (updated)
  • src/scene.cpp (updated)
  • src/shape.cpp (updated)
  • src/sphere.cpp (updated)

Architecture

For me, two different options have been available how to enable the motion into a scene. For the animation, it is crucial to have a time-frame set. My first attemt was to save the animation-length (exposure-time) on the scene and then calculate all the motions with an uniform sampled time between 0 and the specified time. With this approach I had to realize that it is not that easy to calculate and expand the bounding boxes for all the moving shapes correctly because the exposure time was not available during this stage. After rethinking my approach, I decided to store the complete motion on each moving object (e.g. if the ball is moving 4 units on x, the translation is stored as Vector3f(4.0,0.0,0.0)). This simplified the procedure about finding the maximal bounding box in which the objects lies during the complete motion.

Another important thinking was that I want to be able to move the camera as well the objects to be rendered. Of course the moving/rotation of the camera can also be obtained by moving/rotating all the rendered objects in the other way. However, this is not the state of the art and it is simpler to only animate the camera instead of all the other objects. (Additionally: by only moving the camera, no bounding box at all needs to be adjusted/extended. This is only needed when objects move its positions.)

My key idea is to only enable the motion in the integrator. For each object which is moving, a new motion-class can be added. All the properties which are specified as currently in the scene-xml will be taken as start-attributes for this object. In the motion class it can then be specified which property is changed (position, rotation, size, albedo, ...). For the purpose of my final image, I want to apply this motion-blur on a sphere which is rolling on the billiard table. Therefore, I need to implement only the translation as well the rotation. (Also it is enough to implement the motions on the sphere- and camera-class. The same approach can the applied ot other classes as needed).

XML-File

Scene-Properties

Following properties will be available for the scene to add the motion-blur:

enableMotionBlur
True if the motion-blur is enabled and the time for rays is sampled uniformly between \([0,1]\), false if all the rays are sent with the time 0.
motionSamplingByFrame
Two different sampling-methods came in my mind for the motion sampling. One is to divide the time in as many frames as specified in the sampleCount-Property of the scene-sampler. Then for each frame one ray is generated per pixel. To render with this sampling-method, set this property to true. The other sampling method is to completely uniformly sample the time between \([0,1]\) for each ray. Then the property can be set to false or it can be left out. (In my experience for larger motions the uniformly-sampling achieves faster results with less samples. However, for only small motions one can give the frame-sampling a try. Then the sample-count does not need to be that high to render a smooth motion)

Object-Properties

Each object (shape, camera) will have the option to append several instances of the new class "motion" (of course with some restrictions also captured here). This will roughly be structured as described following:

enum type
Possible types of the motion is "rotation", "translation", "size", "albedo" and a few more. Actually I am going to implement only "rotation" and "translation", but others could be implemented in a similar way.

Translation

Vector3f direction
Direction in which the object moves during the rendering process. The length is exactly the length of the motion during the whole rendering-process.

Rotation

Vector3f axis
Direction in which the object moves during the rendering process. The length is exactly the length of the motion during the whole rendering-process.
Vector3f rotationOrigin
How the rotation looks like is depended on the rotation-center. Therefore, it can be specified where this origin is located. Important: The origin is specified by pointing from the object to it. (E.g. the sphere is located at \([1,1,1]^T\) and the rotation-origin is the origin, then this parameter is \([-1,-1,-1]^T\))
float angle
The rotation-angle specified in degrees.

Implementation

include/nori/motion.h and src/motion.cpp

class Motion

Here is the main logic of the motions. Each motion-type is implemented in another class. These all inherits from class Motion;. The main part of this class is the method Eigen::Affine3f Translation::sample(float time). This returns a transformation-matrix for the given motion at given time. This transformation-matrix can then be applied to the vertices in the scene to translate at the correct target location.

class Moveable

This class extends the objects in the scene with the motions read from the xml-file. This motions are then stored with the correct motion-class to be sampled from during the render time. For the current version, it is only possible to specify one animation per motion-type. Also the order is always: rotation before translation.

Ray-sampling

The ray class is extended by a time-property (float). This is 0 in case the motion-blur is disabled. Otherwise it might be any value between 0 and 1 (uniformly sampled). This sampling process is applied when the primary-rays are generated through the renderer in the sample-method of the camera. When the camera is sampled, it is also checked if there exists a motion (currently only translations-supported) for the camera. If so, the origin of the ray is adjusted to the correct camera-center after the translation. (Important: when only the camera is translated, no bounding-box of any object needs to be adjusted/extended)

Adjusting the bounding-boxes

The tree build with the bounding-boxes to speed up the intersection-check is build after reading the scene and before rendering it. It is also build only once and my intention was that it is not good to rebuild this tree for each sampled time. So my idea was to adjust the bounding-box of the elements accordingly before the tree is built.

Before I adjusted the bounding-boxes, the behaviour was quite different between the meshes and the sphere. Because the mesh is saved with each triangle itself, each triangle has a bounding-box on it's own. So by only moving the sphere-mesh a little as an motion, it was not visible anymore. The sphere-class itself has only one big bounding box around the sphere. So only by moving it to far the effect of this became visible.

The bounding boxes can be adjusted in the void activate() const-method. When this method gets invoked, the complete XML-file has been read and the motions are all available through the Moveable-class. For the mesh it can be then iterated through all the triangles and compute the position of it after the motion at time 1. Then the bounding box can be extended correspondingly. The sphere has a pretty equal approach but it can be done much simpler. The current bounding-box is taken and the min- and max-point of it is transformed to the final stage. Then again the min and the max between all possible combinations is taken to expand the bounding box.
Remark: I know that maybe it would be also needed to compute some stages in between to expand the bounding-box correctly. My approach could not render a sphere correctly which is rotating 360 degrees around another origin than itself because the bounding-box would not be expanded at all. However, for my approach for this billiard-table as well the idea that the motion blur is not capturing that large motions, this should be sufficient for now.

Calculate intersections

In the corresponding function bool rayIntersect(uint32_t index, const Ray3f &ray, float &u, float &v, float &t) the object has to be transformed before calculating anything else. For the sphere it is enough to transform the center-point. The rest of the calculations in this method is based only on this point and the radius. (scaling is not yet implemented in th motion-blur-classes, if so => only the radius has to be scaled appropriately) For the mesh it works quite similar. But instead of only transforming the center-point, the vertices of the currently checked triangle needs to be transformed.

To work completely correctly, the same transformations needs to be done in the void setHitInformation(uint32_t index, const Ray3f &ray, Intersection & its)-method. Otherwise the intersection might not match the hit-information.

Results

Translation - Sampling by frames or uniformly

Frames - 8 samples Frames - 128 samples Random - 8 samples Random 128 samples

Rotation - Different speeds

Still Rotating Rotating

Rotation - Different origins

Origin next to Object Origin in the cube itself

Tests

As mentioned in the proposal, my idea was to render a picture independently at time 0 and one at time 1 (start and end). Then I compare the rendered picture with the motion blur and can check if all positions are rendered in between. For this purpose, I used the cube which is spinning around itself in the center. The two top pictures are at the start and end time respectively. The bottom left picture is then a rendered image with motion blur sampled by frame and only with 3 frames. So one can see the start, end and the middle-step. The bottom right picture is also sampled with only 3 samples per pixel, but for each ray another time.

With these tests I found out that I used the wrong time-steps for the frame-sampling. As a result, no ray was sent with the time 1.0, so never rendering the final position in the motion blur. This could be fixed by dividing the time not by m_sampler->getSampleCount(), but by m_sampler->getSampleCount() - 1

By comparing the different start, end and motion-blurred images, I think that it renders quite good and more importantly, correct pictures.

time = 0 time = 1 Frames with 3 samples
time = 0 time = 1 Randomly with 128 samples