Structured light rendering (offline)¶
This tutorial renders a structured-light measurement of the Cornell box with
StructuredLightPathTracerInline, with antithetic sampling.
A projector next to the camera shows fine vertical stripes, \(\cos(2\pi\,u / \lambda)\) with
\(\lambda = 0.01\) in projector coordinates: 100 periods across its field of view. The image shows
only the indirect light (computeDirect off), which is what corrupts real structured-light
measurements. With such fine stripes most of it cancels; what is left varies slowly, except near
the edges, where light bounces between nearby surfaces.
It uses the same scene file as the other tutorials (see Modulated light).
The three images below show what this measurement keeps. With the modulation shifted to \([0, 1]\)
(unsignedModulation), the full measurement (left) is dominated by the direct light, which shows
the projected stripes clearly; its indirect part alone (middle) is smooth and dim. The signed, zero-mean
measurement of this tutorial (right, as above) removes the constant part of the modulation, so only
the indirect light that does not cancel is left. Each image has 8,192 samples per pixel; the script
is modulated_components.py (python modulated_components.py structured_light).
1. Load the scene¶
import matplotlib
matplotlib.use("Agg") # no window
import matplotlib.pyplot as plt
import numpy as np
import falcorcomp as falcor
testbed = falcor.Testbed(create_window=False)
testbed.load_scene("cornell-box/scene-v4-nolight.pbrt")
testbed.resize_frame_buffer(512, 512)
testbed.scene.camera.aspectRatio = 1.0
testbed.clock.pause()
2. Build the render graph¶
VBufferRT finds the primary hits; the projector is set on the tracer itself, so no light pass is
needed:
projectorPosition,projectorDirection,projectorFov,projectorIntensity: a pinhole projector 0.4 to the right of the camera, looking the same way, with a 30-degree field of view, wider than the camera’s 19.5, so it covers the whole view. Without a position, the projector sits at the camera itself (projectorCollocated).pattern,waveform,patternAxis,patternWavelength: sinusoidal stripes along the projector’s \(u\) axis (vertical stripes), with a period of 0.01 in projector coordinates. Gray, XOR, checkerboard and arbitrary binary codes are also available (see the plugin reference).samplingMethod = antithetic: pair every sampled path with the point lit through the mirror image of its stripe position, where the pattern has the opposite sign (see Antithetic sampling).useSingleChannel: keep the luminance, as a single signed value per pixel.
AccumulatePass averages the frames.
graph = testbed.create_render_graph("StructuredLight")
graph.create_pass("VBuffer", "VBufferRT", {"samplePattern": "Center", "sampleCount": 1})
graph.create_pass("Tracer", "StructuredLightPathTracerInline", {
"samplesPerPixel": 8, "maxBounces": 3,
"computeDirect": False, # indirect light only
# A projector 0.4 to the right of the camera, with a wider field of view (the camera's is 19.5).
"projectorPosition": [0.4, 1.0, 6.8], "projectorDirection": [0.0, 0.0, -1.0],
"projectorFov": [30.0, 30.0], "projectorIntensity": [10.0, 10.0, 10.0],
# Vertical cos stripes, 100 periods across the projector's field of view.
"pattern": "periodic", "waveform": "cos", "patternAxis": "u", "patternWavelength": 0.01,
"samplingMethod": "antithetic",
"useSingleChannel": True, "singleChannel": "luminance",
})
graph.create_pass("Accumulate", "AccumulatePass", {"precisionMode": "SingleCompensated"})
graph.add_edge("VBuffer.vbuffer", "Tracer.vbuffer")
graph.add_edge("VBuffer.viewW", "Tracer.viewW")
graph.add_edge("Tracer.color", "Accumulate.input")
graph.mark_output("Accumulate.output")
testbed.render_graph = graph
3. Render¶
for _ in range(1024): # 1024 frames x 8 spp = 8192 spp
testbed.frame()
4. Save the image¶
The measurement is signed, so it is saved as a NumPy array and shown with a diverging colormap whose range is the 95th percentile of its magnitude.
image = graph.get_output("Accumulate.output").to_numpy()[..., 0] # one channel (luminance)
np.save("structured_light.npy", image)
# The measurement is signed: show it with a diverging colormap, 0 in white.
limit = np.percentile(np.abs(image), 95)
plt.imsave("structured_light.png", image, cmap="bwr", vmin=-limit, vmax=limit)
The full script: structured_light_offline.py.
For an equal-time comparison of antithetic and naive sampling, and how it changes with the pattern, see Antithetic sampling for modulated light.