Laser visualization (offline)

This tutorial draws the laser’s beam over a time-gated image of the Cornell box with LaserPositionViewer. Seeing the beam helps to place the laser, and to read a time-gated image: the gate selects the light that left the laser spot a given time ago.

It uses the same scene file as the other tutorials (see ToF rendering).

A time-gated image, the same image with the collimated beam drawn over it, and with a 2-degree cone and its spot

1. Load the scene and move the camera

The scene’s camera sits next to the laser and looks along the beam, so the beam would show as a single point. The script moves the camera above and to the right of the laser, and gives it a wider lens than the scene’s, so that the beam crosses the image on its way into the box.

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()

# The scene's camera looks down the laser beam; look from above and to the right
# instead, so that the beam crosses the image.
camera = testbed.scene.camera
camera.position = falcor.float3(1.6, 2.2, 8.5)
camera.target = falcor.float3(0.0, 1.0, 0.0)
camera.up = falcor.float3(0.0, 1.0, 0.0)
camera.focalLength = 60.0  # mm

2. Build the render graph

The time-gated path tracer and its accumulation are those of Time-gated rendering (offline), with a gate for this camera: the path length of the light that comes straight back from the laser spot is about 17.4 here, and the gate at 18.5 keeps the light that bounced once more near the spot.

LaserPositionViewer draws over the accumulated image. It reads the laser that LaserLight publishes every frame, so it needs the execution edge add_edge("Laser", "Viewer"), and the V-buffer, to stop the beam where it goes behind the scene. The laser is collimated (laserAngle 0), a beam of no width: beamRadius gives it a width to draw and coneDensity makes it solid. The tone mapper comes after the viewer, so the beam is drawn on linear radiance.

graph = testbed.create_render_graph("LaserVisualization")
graph.create_pass("VBuffer", "VBufferRT", {"samplePattern": "Center", "sampleCount": 1})
graph.create_pass("Laser", "LaserLight", {
    "laserPosition": [0.0, 1.7, 6.8], "laserDirection": [0.0, 0.0, -1.0],
    "laserPower": [170.0, 120.0, 40.0], "laserAngle": 0.0,
})
graph.create_pass("Tracer", "TimeGatedPathTracerInline", {
    "samplesPerPixel": 16, "maxBounces": 6, "computeDirect": False,
    "timeGateMode": "box", "timeGateWindow": 0.1, "timeMin": 18.5, "timeMax": 18.5,
})
graph.create_pass("Accumulate", "AccumulatePass", {"precisionMode": "SingleCompensated"})
# LaserPositionViewer draws the laser's beam over its input, like light in fog.
graph.create_pass("Viewer", "LaserPositionViewer", {
    "showCone": True, "coneColor": [1.0, 0.0, 0.0],
    "beamRadius": 0.02, "coneDensity": 20.0,
})
graph.create_pass("ToneMapper", "ToneMapper", {"autoExposure": False})

graph.add_edge("VBuffer.vbuffer", "Tracer.vbuffer")
graph.add_edge("VBuffer.viewW", "Tracer.viewW")
graph.add_edge("Laser", "Tracer")
graph.add_edge("Tracer.color", "Accumulate.input")
# The viewer needs the laser (execution edge) and the V-buffer, to stop the beam
# where the scene hides it.
graph.add_edge("Laser", "Viewer")
graph.add_edge("VBuffer.vbuffer", "Viewer.vbuffer")
graph.add_edge("VBuffer.viewW", "Viewer.viewW")
graph.add_edge("Accumulate.output", "Viewer.input")
graph.add_edge("Viewer.output", "ToneMapper.src")
graph.mark_output("Accumulate.output")  # output 0: the time-gated image alone
graph.mark_output("ToneMapper.dst")     # output 1: with the laser, tone mapped
testbed.render_graph = graph

3. Render and save

The overlay is redrawn every frame, so it does not need to be accumulated. Output 0 is the time-gated image alone (EXR), output 1 the image with the beam (PNG).

for _ in range(256):  # 256 frames x 16 spp = 4096 spp
    testbed.frame()

# 4. Save the images
testbed.capture_output("time_gated.exr", 0)
testbed.capture_output("laser_visualization.png", 1)

A diverging laser

With a cone angle, the laser lights a spot instead of a point, and the viewer draws the cone and, with showSpot (on by default), the spot: the light the laser puts on each surface, without shadows. The right image above sets "laserAngle": 2.0 on LaserLight and a translucent cone on the viewer:

graph.create_pass("Viewer", "LaserPositionViewer", {
    "showCone": True, "beamRadius": 0.02, "coneDensity": 1.0,
    "showSpot": True, "spotScale": 20.0,
})

The time-gated image changes too, as the laser now lights the whole spot. See LaserPositionViewer for all the options.

The full script: laser_visualization_offline.py.