Doppler ToF path tracer (DopplerToFPathTracerInline)¶
This render pass simulates a Doppler time-of-flight camera: a continuous-wave ToF camera whose light is modulated at frequency \(f_g\) and whose sensor is modulated at a slightly different frequency \(f_g - \Delta f\). After the sensor’s low-pass filter, a path of optical length \(\ell\) (meters) arriving at time \(t\) is weighted by
and over the exposure \([0, T)\) the camera measures
When \(T\) is a whole number of heterodyne periods \(1/\Delta f\), light from static objects cancels, and what remains
comes from paths whose length changes during the exposure: the Doppler signal. Unlike
DopplerHistogramPathTracerInline, the objects really move: every frame draws
a time \(t\), moves the objects (see Velocities) to their pose at \(t\), and traces the paths
there.
Antithetic time pairs¶
The heterodyne term changes sign over half a period: \(w(t + 1/(2\Delta f), \ell) = -w(t, \ell)\). With
antithetic = half_period, every frame renders the time \(t\) and its antithetic time \(t + 1/(2\Delta f)\) (modulo \(T\)) and
outputs their mean, so light from static objects cancels within each pair. With randomReplay, the antithetic time uses
the same random numbers (random replay), so the two paths are the same up to the motion in between. On the Cornell box
with moving boxes (200 MHz, 50 ms exposure), pairing lowered the error of the heterodyne image 17 to 50 times at
equal render count, and random replay a further 1.1 to 1.3 times.
Parameters¶
Measurement:
Parameter |
Type |
Description |
|---|---|---|
|
float |
Light modulation frequency \(f_g\), MHz. (Default: |
|
float |
\(\Delta f\), Hz: the light’s modulation frequency minus the sensor’s. |
|
float |
Exposure \(T\), s. Static light cancels only when \(T\,\Delta f\) is a whole number; the pass warns otherwise.
(Default: |
|
float |
Sensor phase \(\phi\), in periods ( |
|
dictionary |
The motion of the scene objects; see Velocities. (Default: none) |
Time sampling:
Parameter |
Type |
Description |
|---|---|---|
|
string |
How each frame’s time \(t\) is drawn: |
|
string |
|
|
boolean |
The antithetic time uses the same random numbers as the first. (Default: |
|
integer |
Offsets the time sequence and the random numbers of the paths. (Default: |
Sampling and output, as for the time-gated path tracer:
samplesPerPixel (default 128), maxBounces (default 3), computeDirect (default false),
useImportanceSampling, useAlphaTest, and useSingleChannel with singleChannel, which write the chosen channel
to all three channels of the output. Each frame traces the pixel-center camera ray itself, in the scene at time \(t\),
so the pass takes no V-buffer.
Moving objects¶
Every frame sets the scene-graph nodes of the moving objects to their pose at \(t\), updates the scene and its acceleration structure, and puts the objects back at their pose at \(t = 0\) after the frame. This needs:
Animated objects. Falcor bakes static meshes into one merged acceleration structure, which cannot move. Build the moving objects as animated, for example
sceneBuilder.addTriangleMesh(mesh, material, True)in a.pyscene(the third argument isisAnimated). pbrt scenes cannot do this.One node per moving object. Load the scene with
SceneBuilderFlags.DontMergeMaterials | SceneBuilderFlags.DontOptimizeGraph; otherwise the graph optimizer can merge objects into one node, which then move together.
Motions are in world space for every node, also one with a parent. A moving object below another moving object in the scene graph keeps its own motion: list both with the same motion to move them together. An object that is not listed but hangs below a moving object (or shares its node) moves with it here, while the Doppler passes, which only read the listed velocities, see it at rest.
The pass warns when a named object is static, when two objects on one node are given different motions, and when an
unlisted object moves with a listed one; get_object_names() returns (instance, mesh name, material name, movable)
for every object. The camera and the light do not move.
The scene changes every frame, so an AccumulatePass after this pass restarts every frame unless it is created with
"autoReset": False.
From Python: set_velocity(...), clear_velocities(), get_object_names() and reset(), which restarts the time
sequence (the next frame is pair 0).
Velocity from heterodyne and homodyne¶
For a path whose length changes at the rate \(\mathrm{d}\ell/\mathrm{d}t\), the frequency shift is \(\varepsilon = -f_g\, (\mathrm{d}\ell/\mathrm{d}t) / c\). With complex measurements \(C = I_{\phi = 0} + i\, I_{\phi = 0.25}\), a heterodyne (\(\Delta f = 1/T\)) and a homodyne (\(\Delta f = 0\)) measurement give
where \(v\) is the velocity along the pixel ray for a light next to the camera. A single phase works too, except where the homodyne image crosses zero. The relation assumes each path’s intensity is constant during the exposure; see the Doppler ToF tutorial for how close it gets.
Inputs and outputs¶
Channel |
Description |
|---|---|
|
The Doppler ToF measurement of the frame (signed), RGBA32Float. Averaging frames integrates over the exposure. |
The light is set on the LaserLight pass, as for the time-gated path tracer.
Example¶
testbed.load_scene("cornell-box-moving/scene.pyscene",
falcor.SceneBuilderFlags.DontMergeMaterials | falcor.SceneBuilderFlags.DontOptimizeGraph)
graph = testbed.create_render_graph("DopplerToF")
graph.create_pass("Light", "LaserLight", {"isLightSourceLaser": False, "laserCollocated": True})
graph.create_pass("Tracer", "DopplerToFPathTracerInline", {
"samplesPerPixel": 16, "maxBounces": 3, "computeDirect": True,
"modulationFrequency": 30.0, "heterodyneFrequency": 1.0 / 0.015, "exposureTime": 0.015,
"antithetic": "half_period", "randomReplay": True,
"velocities": {"TallBox": {"linear": [0.0, 0.0, 2.0]}},
})
graph.create_pass("Accumulate", "AccumulatePass", {"autoReset": False})
graph.add_edge("Light", "Tracer")
graph.add_edge("Tracer.color", "Accumulate.input")
graph.mark_output("Accumulate.output")
testbed.render_graph = graph
See the Doppler ToF tutorial for a complete script.