Develop
Develop
Select your platform

Customize Passthrough

Styling, composite layering, and surface-projected passthrough are a few ways to customize passthrough.

Styling Passthrough

The visual style of passthrough layers can be customized using the xrPassthroughLayerSetStyleFB function. The following options for stylization are available:
  • Texture opacity: Change the opacity of the passthrough image (independent of the edge rendering) using textureOpacityFactor.
  • Edge rendering: Runs an edge detection algorithm on the passthrough image and superimposes the result on top of it. Set XrPassthroughStyleFB::edgeColor::a to 0 to disable edge rendering.
  • Color reproduction: Customize the color reproduction of the passthrough image by adding one of the following types into the next chain of XrPassthroughStyleFB:
XrPassthroughColorMapMonoToMonoFB and XrPassthroughColorMapMonoToRgbaFB force a conversion to grayscale, which typically makes them inappropriate for use on devices that support color passthrough. Unless grayscale passthrough is explicitly desired, prefer using XrPassthroughBrightnessContrastSaturationFB, XrPassthroughColorMapLutMETA, or XrPassthroughColorMapInterpolatedLutMETA.
Color reproduction effects are mutually exclusive. Only one of the aforementioned structs can be added to the next chain of XrPassthroughStyleFB, otherwise the call to xrPassthroughLayerSetStyleFB will fail. You can disable these effects by calling xrPassthroughLayerSetStyleFB again without any structs in the next chain.
Simple forms of color control (for example, tinting) can also be achieved using the XR_KHR_composition_layer_color_scale_bias extension. An advantage of using that extension is that changes take effect immediately, while setting a style using xrPassthroughLayerSetStyleFB may only take effect in the next frame. Note that using color scale and bias for color and alpha differs from the style’s color mapping feature and textureOpacityFactor in subtle ways:
  • Color scale is applied to the entire layer contents, including edge rendering, while the corresponding style features only affect the passthrough images.
  • When Surface-projected passthrough is used, the style’s textureOpacityFactor is applied individually to each surface/triangle, which can lead to visible overlaps between surfaces. On the other hand, the alpha value of color scale is multiplied with the passthrough layer after it has been fully rendered (flattened), leading to compositing that’s akin to layering in 2D graphics software.

Using Color Look-Up Tables (LUTs)

A color LUT is a 3D array which maps each RGB color to an arbitrary new RGB(A) color. Color LUTs enable a wide range of effects, ranging from subtle color grading to stylizations such as posterization, selective coloring, and chroma keying.
Color LUTs must first be created using xrCreatePassthroughColorLutMETA before they can be applied to a passthrough layer using xrPassthroughLayerSetStyleFB. The following snippet creates and applies a color LUT which applies a purple tint to passthrough:
// Define the color LUT data
constexpr size_t kResolution = 32;
constexpr size_t kSizeOfRgbPixel = 3;
constexpr float kNormalizationFactor = 255.0f / (kResolution - 1);
std::vector<uint8_t> lutData;
lutData.reserve(kResolution * kResolution * kResolution * kSizeOfRgbPixel);
for (size_t b = 0; b < kResolution; ++b) {
  for (size_t g = 0; g < kResolution; ++g) {
    for (size_t r = 0; r < kResolution; ++r) {
      lutData.push_back(std::min(r * kNormalizationFactor, 255.0f));        // red
      lutData.push_back(std::min(g * kNormalizationFactor * 0.3f, 255.0f)); // green
      lutData.push_back(std::min(b * kNormalizationFactor, 255.0f));        // blue
    }
  }
}

// Create the color LUT
XrPassthroughColorLutMETA lutHandle;
XrPassthroughColorLutCreateInfoMETA createInfo{XR_TYPE_PASSTHROUGH_COLOR_LUT_CREATE_INFO_META};
createInfo.channels = XR_PASSTHROUGH_COLOR_LUT_CHANNELS_RGB_META;
createInfo.resolution = kResolution;
createInfo.data.buffer = lutData.data();
createInfo.data.bufferSize = lutData.size();
XrResult result = xrCreatePassthroughColorLutMETA(passthroughFeature, &createInfo, &lutHandle);
if (XR_FAILED(result)) {
  LOG(“Failed to create a passthrough color LUT.”);
}

// Apply the color LUT to a passthrough layer
XrPassthroughColorMapLutMETA colorLutStyle{XR_TYPE_PASSTHROUGH_COLOR_MAP_LUT_META};
colorLutStyle.colorLut = lutHandle;
colorLutStyle.weight = 1.0f;

XrPassthroughStyleFB passthroughStyle{XR_TYPE_PASSTHROUGH_STYLE_FB};
passthroughStyle.textureOpacityFactor = 1.0f;
passthroughStyle.next = &colorLutStyle;

XrResult result = xrPassthroughLayerSetStyleFB(passthroughLayer, &passthroughStyle);
if (XR_FAILED(result)) {
  LOG(“Failed to set style on a passthrough layer.”);
}
The resolution of a color LUT is the number of color values present in the LUT for each input color channel. For example, a resolution of 32 corresponds to a 3D array of 32^3 color values. The output color value for an input color triplet (R, G, B) must be stored at the index lut[(R + G * resolution + B * resolution * resolution) * bytesPerColor], where bytesPerColor is either 3 or 4 depending on XrPassthroughColorLutCreateInfoMETA::channels. The headset will tri-linearly interpolate the color LUT values when performing a look-up with a higher input bit depth.
There is a constraint on the maximum resolution allowed for color LUTs, which can be queried using XrSystemPassthroughColorLutPropertiesMETA. On current Meta Quest devices, the maximum resolution is 64. The color LUT resolution impacts memory usage and GPU performance. For that reason, it is advisable to keep the resolution as small as possible given use case and quality constraints. For example, start with a color LUT of resolution 16, then check if increasing the resolution to 32 significantly improves the visual quality.

Animating Color LUT Transitions

The weight parameter in XrPassthroughColorMapLutMETA defines the blend between the original passthrough colors and the LUT colors. Use it to smoothly fade a color LUT in or out by calling xrPassthroughLayerSetStyleFB with a varying weight value in each frame.
To achieve a smooth transition between two color LUTs, use XrPassthroughColorMapInterpolatedLutMETA and vary the weight parameter in the same fashion. The blend between the two color LUTs is computed as
C_out = (1 - weight) * sourceColorLut[C_in] + weight * targetColorLut[C_in]

Animating Color LUT Contents

Some use cases require color LUTs to change continuously beyond linear interpolation. For example, an app could dynamically adjust the saturation of different color hues based on the state of the app or external input (such as audio).
Call xrUpdatePassthroughColorLutMETA to update the LUT data of an existing color LUT. The updated data will automatically be used on all passthrough layers which have the corresponding XrPassthroughColorLutMETA applied. There is no need to call xrPassthroughLayerSetStyleFB to propagate the changes.
For high resolution color LUTs (especially 64), calling xrUpdatePassthroughColorLutMETA in every frame can have a notable performance impact. Try to keep the LUTs as small as possible in such cases - a resolution of 32 or lower is recommended.

Additional Notes for Color LUT Usage

  • For high resolution color LUTs (especially 64), xrCreatePassthroughColorLutMETA can take a few milliseconds and is best done in advance, outside of time-critical sections.
  • An instance of XrPassthroughColorLutMETA instances occupy an amount of memory that is proportional to the LUT data size. Call xrDestroyPassthroughColorLutMETA to free this memory.
  • Color LUTs can be applied to multiple passthrough layers by referencing it in xrPassthroughLayerSetStyleFB calls on multiple layers. Doing so reduces the memory usage (compared to creating a separate XrPassthroughColorLutMETA instance per layer).

Compositing and Masking for Mixed Reality

By default, a passthrough layer (using automatic environment reconstruction) covers the entire screen. There are multiple means to combine passthrough and VR in interesting ways to create mixed reality experiences:
  • Setting the layers’ opacity values to create a full-screen blend between passthrough and VR by using styling for passthrough layers or XR_KHR_composition_layer_color_scale_bias for any layer type.
  • Masking passthrough layers, either with the application’s alpha channel or with a separate projection layer containing only the alpha mask.
  • Using surface-projected passthrough.

Masked Passthrough using a Separate Mask Layer

Since passthrough layers are rendered by the system, an application cannot directly control the contents of the alpha channel beyond the means offered by styling passthrough. However, applications can submit an alpha mask in a separate projection layer and customize the blend functions to blend the passthrough layer with that alpha mask.
  • Draw the alpha mask to a separate projection layer and submit it between the passthrough layer and the application layer: Alpa layer in a separate projection layer
  • Using XR_FB_composition_layer_alpha_blend, set the blend factors for the alpha mask layer such that the color values remain unchanged and the alpha values are overwritten:
alphaBlend.srcFactorColor = XR_BLEND_FACTOR_ZERO_FB;
alphaBlend.dstFactorColor = XR_BLEND_FACTOR_ONE_FB;
alphaBlend.srcFactorAlpha = XR_BLEND_FACTOR_ONE_FB;
alphaBlend.dstFactorAlpha = XR_BLEND_FACTOR_ZERO_FB;
  • Using the same extension, set the blend factors for the passthrough compositor layer such that the source color is multiplied with the destination alpha value:
alphaBlend.srcFactorColor = XR_BLEND_FACTOR_DST_ALPHA_FB;
alphaBlend.dstFactorColor = XR_BLEND_FACTOR_ONE_MINUS_DST_ALPHA_FB;
alphaBlend.srcFactorAlpha = XR_BLEND_FACTOR_ONE_FB;
alphaBlend.dstFactorAlpha = XR_BLEND_FACTOR_ZERO_FB;
This approach is very flexible and extends to multiple passthrough layers and arbitrary layer ordering. However, submitting additional layers to the XR compositor incurs a significant performance overhead. It is therefore preferable to use the app’s alpha channel, mentioned in the following section.

Masked Passthrough using Alpha Channel

The most efficient way to make passthrough show up only in certain regions of the screen, for example, through a passthrough window geometry, is to punch holes into the app’s projection layer through which an underlying passthrough layer becomes visible.
  • Create a passthrough layer and place it before the application layer in the layers list in xrEndFrame: Native passthrough custom
  • After drawing the virtual world to the framebuffer, make an extra draw pass which clears out (or reduces) the alpha values in regions where passthrough should be visible.
    • For example, you can render a shape with a shader that returns an alpha value of 0 and a blend mode which causes the alpha value in the framebuffer to be replaced with the shader output and the color to remain unchanged.
  • The application layer is commonly submitted as premultiplied by alpha. This leads to the source color values not getting attenuated by the alpha value in regions where the alpha value has been reduced in the previous step. There are two alternative solutions to this problem:
  • Switch the application layer to unpremultiplied alpha by setting XR_COMPOSITION_LAYER_UNPREMULTIPLIED_ALPHA_BIT (and, if XR_FB_composition_layer_alpha_blend is used, picking the appropriate blend factors). This changes the interpretation of the colors for the entire layer, so the application needs to ensure that all color values in the framebuffer are not premultiplied with alpha.
  • In the previous step, where the alpha value is modified, also modify the color value to make it reflect the premultiplied state. For example, multiply both color and alpha with the desired (inverse) passthrough opacity.

Occlusion

The Passthrough API doesn’t offer a solution to resolve occlusions between virtual and real objects by default. However, if the application is in the possession of the approximate geometry of real-world objects, this information can be used to occlude virtual objects during rendering.
For example, tracked hands can be used as an occluder in VR, causing hands to appear as passthrough when they are in front of virtual objects, by drawing the hand mesh to the depth buffer only (with disabled color writes) before drawing any VR elements.
The layering between passthrough and VR can be made arbitrarily complex by alternating between rendering VR objects and passthrough occluders, and by utilizing the depth write/test toggles. For example, a virtual watch could be drawn on top of the passthrough hand from the previous paragraph by rendering the 3D model of a watch either before the hand occluder, or with depth testing disabled such that it doesn’t get occluded by the hand.

Surface-Projected Passthrough

Surface-projected passthrough allows apps to specify the geometry onto which the passthrough images are projected (instead of an automatic environment depth reconstruction). For surface-projected passthrough layers, passthrough is only visible within the specified surface geometries, the rest of the layer is transparent.
Surface-projected passthrough can be used when the exact locations of real-world features are known (for example, a desk marked by the user using controllers) to avoid visual artifacts that may arise from the dynamic environment reconstruction used by regular passthrough layers. The surface geometries provided by the app should match real-world surfaces as closely as possible. If they differ significantly, users will receive conflicting depth cues, and objects may appear too small or large. On Quest Pro, such mismatches also lead to a shift between the color and luminance images, making colored objects appear in the wrong location.
There is no depth testing between the passthrough projection surface and the objects rendered in VR. This leads to surface-projected passthrough rendering either as an underlay (always occluded by virtual objects) or overlay (always occludes virtual objects).
You can add the following example script to GameObjects to render them as passthrough.
The following steps are needed to use surface-projected passthrough:
  • Request the extension XR_FB_triangle_mesh when creating the OpenXR instance in the app code (see Creating an Instance and Session).
  • When creating the passthrough layer, set XrPassthroughLayerCreateInfoFB::purpose to XR_PASSTHROUGH_LAYER_PURPOSE_PROJECTED_FB to activate surface-projected passthrough for the layer. The layer will not be visible until geometries are created for the projection surface.
  • Create a triangle mesh object using xrCreateTriangleMeshFB.
  • Add an instance of the triangle mesh to the projection surface using xrCreateGeometryInstanceFB.
An application can create multiple instances of the same triangle mesh object. Each instance comes with its own transformation, which can be updated using xrGeometryInstanceSetTransformFB.
Did you find this page helpful?
Thumbs up icon
Thumbs down icon