Skip to content

Latest commit

 

History

History
714 lines (500 loc) · 28.9 KB

File metadata and controls

714 lines (500 loc) · 28.9 KB

How To Examples

These examples show how to use Hydra Viewport Toolbox (HVT) tasks, helpers, and workflows in application code. HVT both simplifies OpenUSD Hydra viewport integration and extends OpenUSD with additional features (e.g. WBOIT, outlines) and capabilities (e.g. geometry helpers, scene-index filters).

Each How-to lives in howTos/ as a runnable source file and is also compiled into the test binary — but its primary purpose is demonstration, not exhaustive validation.

For traditional unit and regression testing, see tests/. That directory contains many tests per feature (params equality, construction, edge cases, image baselines). There is typically one How-to per feature.

ℹ️ The list of examples is ordered from very basic first steps to much more advanced concepts. You do not need to follow the order if you look for a specific topic.

Table of content

How to compile Hydra Viewport Toolbox in my environment

Clone the repo

Below are the usual steps to correctly create your local clone of Hydra Viewport Toolbox.

git clone --recurse-submodules https://github.com/Autodesk/hydra-viewport-toolbox.git hvt
cd hvt
cmake --preset debug
cmake --build --preset debug

Use a locally built USD

Now, you have to specify that you want to build Viewport Toolbox from your local USD using the cmake option OPENUSD_INSTALL_PATH which must point to the install directory of your local compilation.

cmake -GNinja -DOPENUSD_INSTALL_PATH=../../usd_1/_install -DCMAKE_INSTALL_PREFIX=../_install ../.
ninja
ninja install

Easy debugging

How-tos and unit tests share the same harness. To run or debug them:

./bin/hvt_test

or

ctest --preset debug

Advanced topics

ℹ️ For more details such as platform specific aspects, refer to the project README.

How to create an Hgi instance

This example (refer to HowTo01_CreateHgiImplementation.cpp for the implementation details) demonstrates how to create and destroy an Hgi (i.e., Hydra Graphics Interface) implementation. In fact, the Hgi API is an abstraction of a backend (e.g., OpenGL, Metal, etc.).

⚠️ Only one Hgi instance must be created.

The way to initialize the default pxr::Hgi implementation is:

pxr::HgiUniquePtr hgi = pxr::Hgi::CreatePlatformDefaultHgi();

The way to initialize a specific pxr::Hgi implementation is:

pxr::HgiUniquePtr hgi = pxr::Hgi::CreateNamedHgi(pxr::HgiTokens->Metal);

ℹ️ To have the complete list of supported backends, check here

The way to declare an pxr::HdDriver is:

pxr::HdDriver hgiDriver;
hgiDriver.name   = pxr::HgiTokens->renderDriver;
hgiDriver.driver = pxr::VtValue(hgi.get());

How to create one frame pass

This example (refer to HowTo02_CreateOneFramePass.cpp for implementation details) demonstrates how to create one frame pass using the Storm render delegate.

Create the frame pass

There are three steps to fully create a frame pass from an arbitrary USD scene file.

Step 1: Below is the code to create a render index instance for a specific hgi implementation with Storm as the renderer.

hvt::RenderIndexProxyPtr renderIndex;
hvt::RendererDescriptor renderDesc;
renderDesc.hgiDriver    = &hgiDriver;
renderDesc.rendererName = "HdStormRendererPlugin";
hvt::ViewportEngine::CreateRenderer(renderIndex, renderDesc);

Step 2: Below is the code to create a scene index.

pxr::HdSceneIndexBaseRefPtr sceneIndex = hvt::ViewportEngine::CreateUSDSceneIndex(stage);
renderIndex->RenderIndex()->InsertSceneIndex(sceneIndex, pxr::SdfPath::AbsoluteRootPath());

Step 3: Below is the code to create a frame pass.

A frame pass is the class used to render (or select) a collection of prims using a set of input parameters and render state. The class internally contains a task controller to generate the list of render tasks and an engine to render them.

hvt::FramePassDescriptor passDesc;
passDesc.renderIndex = renderIndex->RenderIndex();
passDesc.uid         = pxr::SdfPath("/sceneFramePass");
hvt::FramePassPtr sceneFramePass = hvt::ViewportEngine::CreateFramePass(passDesc);

Update the frame pass

Once a frame pass is created its parameters can be set on it to control how the rendering and selection are performed. Default values are fine unless the rendering step needs values different from the defaults. For example, the view and projection matrices must change after loading a new scene or moving the camera.

auto& params = sceneFramePass->params();

params.renderBufferSize          = pxr::GfVec2i(context.width(), context.height());
params.viewInfo.framing          = hvt::ViewParams::GetDefaultFraming(context.width(), context.height());
params.viewInfo.viewMatrix       = stage.viewMatrix();
params.viewInfo.projectionMatrix = stage.projectionMatrix();

<...>

ℹ️ The complete list of parameters are defined here

Render the frame pass

The code must now create all the render tasks and render them by doing:

sceneFramePass->Render();

In fact the call is decomposed in two steps:

  • const pxr::HdTaskSharedPtrVector renderTasks = sceneFramePass->GetRenderTasks(); to get the list of render tasks.
  • sceneFramePass->Render(renderTasks); to sequentially execute the render tasks.

How to create two frame passes

This example (refer to HowTo03_CreateTwoFramePasses.cpp for implementation details) demonstrates how to create two frame passes.

In order to create two frame passes, the idea is to repeat the HowTo02 example to create the two frame passes, use the HowTo01b example to load the selected manipulator asset and finally, to add the needed glue between the two frame passes to make it work.

Select the render of one of the frame pass

When creating the render index a dev can select a specific render to use instead of the default one i.e., Storm by updating the following line:

renderDesc.rendererName = "HdStormRendererPlugin";

When having multiple frame passes, dev can select a renderer for the scene pass and another one for the secondary pass(es). For example it could be HdEmbreeRendererPlugin for the scene pass and the default HdStormRendererPlugin for all the secondary frame passes.

Update & render the frame passes

As explained in the previous example, the code below updates the first render frame but it delays the display to let the second frame pass also update the render buffers.

{
    auto& params = framePass1->params();

    <...>

    // Do not display right now, wait for the second frame pass.
    params.enablePresentation = false;

    framePass1->Render();
}

The code below gets the color and depth render buffers from the first frame pass so the second frame uses them. That's for now the default composition between frame passes.

// Get the input AOV's from the first frame pass and use them in all overlays so the
// overlay's draw into the same color and depth buffers.

auto& pass = mainFramePass.sceneFramePass;
hvt::RenderBufferBindings inputAOVs = pass->GetRenderBufferBindingsForNextPass(
    { pxr::HdAovTokens->color, pxr::HdAovTokens->depth });

Finally, the code below updates the second frame pass, renders without clearing the background as the render buffers already contain the final render of the first frame pass, and finally displays the result (i.e. by default params.enablePresentation is true).

{
    auto& params = framePass2->params();

    <...>

    // Do not clear the background as it contains the previous frame pass result.
    params.clearBackground = false;
    params.backgroundColor = pxr::GfVec4f(0.0f, 0.0f, 0.0f, 0.0f);

    // Get the list of tasks to render but use the render buffers from the main frame pass.
    const pxr::HdTaskSharedPtrVector renderTasks = framePass2->GetRenderTasks(inputAOVs);

    framePass2->Render(renderTasks);
}

How to create a custom render task

This example (refer to HowTo04_CreateACustomRenderTask.cpp for implementation details) demonstrates how to create and render a custom render task like the blur task from the Viewport Toolbox resources.

Take the HowTo02 example to create the frame pass and then, add the custom render task.

Implement it

As the blur task is a rendering task there are mainly two parts to create i.e., the glslfx containing the shader program and the task itself i.e., pxr::HdxTask.

As example:

  • The blur shader program is here.
  • The blur task include is here

There are only few virtual methods to implement.

/// The blur parameters.
struct VIEWPORT_Export BlurTaskParams
{
    /// The amount of Blur to apply.
    float blurAmount = 0.5f;

    /// The name of the aov to blur.
    pxr::TfToken aovName = pxr::HdAovTokens->color;
};

/// The blur render task.
class BlurTask : public pxr::HdxTask
{
public:
    BlurTask(pxr::HdSceneDelegate* delegate, pxr::SdfPath const& id);
    ~BlurTask() override;

    void Prepare(pxr::HdTaskContext* ctx, pxr::HdRenderIndex* renderIndex) override { ... }

    void Execute(pxr::HdTaskContext* ctx) override { ... }

protected:
    void _Sync(pxr::HdSceneDelegate* delegate, pxr::HdTaskContext* ctx, 
        pxr::HdDirtyBits* dirtyBits) override { ...}

    BlurTaskParams _params;
};
  • The _Sync() synchronizes the blur parameters with the render task parameters.
  • The Prepare() performs any preprocessing works (before the execution).
  • The Execute() applies the blur image processing i.e. it executes the shader program in that case.

In most cases, the _Sync() implementation can be quite generic:

void BlurTask::_Sync(HdSceneDelegate* delegate, HdTaskContext* ctx, HdDirtyBits* dirtyBits)
{
    if ((*dirtyBits) & HdChangeTracker::DirtyParams)
    {
        BlurTaskParams params;
        if (_GetTaskParams(delegate, &params))
        {
            _params = params;
        }
    }

    *dirtyBits = HdChangeTracker::Clean;
}

Add it in the frame pass

The code below adds the blur task to the frame pass.

// Adds the 'blur' custom task to the frame pass.

{
    // Defines the blur task update function.

    // In that case, there is no need for any update.
    auto fnCommit =
        [&](hvt::TaskManager::GetTaskValueFn const& /*fnGetValue*/,
            hvt::TaskManager::SetTaskValueFn const& /*fnSetValue*/) {};

    // Adds the blur task i.e., 'blurTask' before the color correction one.

    const pxr::SdfPath colorCorrectionTask =
        main->GetTaskManager()->GetTaskPath("colorCorrectionTask");

    const pxr::SdfPath blurPath =
        main->GetTaskManager()->AddTask<hvt::BlurTask>(
            hvt::BlurTask::GetToken(), fnCommit, colorCorrectionTask,
            hvt::TaskManager::InsertionOrder::insertBefore);

    // Sets the default value.

    hvt::BlurTaskParams blurParams;
    blurParams.blurAmount = 8.0f;
    main->GetTaskManager()->SetTaskValue(blurPath, pxr::HdTokens->params, pxr::VtValue(blurParams));
}

When the blur value is dynamically changeable, the update method (i.e., fnCommit) can be:

// Lets define the application parameters.
struct AppParams
{
    float blur { 8.0f };
    //...
} app;

// Defines the update function callback.
auto fnCommit =
    [&](hvt::TaskManager::GetTaskValueFn const& fnGetValue,
        hvt::TaskManager::SetTaskValueFn const& fnSetValue) {
        const pxr::VtValue value = fnGetValue(pxr::HdTokens->params);
        hvt::BlurTaskParams params = value.Get<hvt::BlurTaskParams>();
        params.blurAmount = app.blur;
        fnSetValue(pxr::HdTokens->params, pxr::VtValue(params));
    };

If the app needs to enable/disable the task, the code is:

const pxr::SdfPath blurTask = main->GetTaskManager()->GetTaskPath("blurTask");
main->GetTaskManager()->EnableTask(blurTask, myApp->enableBlur);

The last step is to render the frame pass using the updated list of render passes.

// Renders the updated list of render tasks.
sceneFramePass->Render();

ℹ️ The last step encapsulates the pxr::HdEngine usage.

It uses the frame pass engine instance to execute the list of render tasks.

float FramePass::Render(const pxr::HdTaskSharedPtrVector& renderTasks)
{
    // Render using a list of render tasks.
    _engine->Execute(
        _taskController->GetRenderIndex(), const_cast<HdTaskSharedPtrVector*>(&renderTasks));
    return 1.0f;
}

Internally, the engine updates the render task parameters, prepares them and renders them. To better understand the code could be simplified like:

Engine::Execute(HdRenderIndex *index, HdTaskSharedPtrVector *tasks)
{
    index->SyncAll(tasks, &_taskContext);

    for (size_t taskNum = 0; taskNum < numTasks; ++taskNum)
    {
        const HdTaskSharedPtr &task = (*tasks)[taskNum];
        task->Prepare(&_taskContext, index);
    }

    for (size_t taskNum = 0; taskNum < numTasks; ++taskNum)
    {
        const HdTaskSharedPtr &task = (*tasks)[taskNum];
        task->Execute(&_taskContext);
    }
}

How to use the SSAO (Ambient Occlusion) task

This example (refer to HowTo05_UseSSAORenderTask.cpp for implementation details) demonstrates how to use the SSAO (i.e., Ambient Occlusion render task) task from the Hydra Viewport Toolbox resources.

ℹ️ Specifically, Hydra Viewport Toolbox task implements "screen-space ambient occlusion" (SSAO), which computes ambient occlusion in real-time using image-space information.

Follow the HowTo02 example to create a frame pass and then add the SSAO task as a custom render task. To visualize the ambient occlusion (ao) buffer only, the variable in isShowOnlyEnabled needs to be true. It will update a flag in the shader and output the occlusion result only.

 // Adds ssao custom task to the frame pass

 {
     // Defines ssao task update function

     auto fnCommit = [&](hvt::TaskManager::GetTaskValueFn const& fnGetValue,
                         hvt::TaskManager::SetTaskValueFn const& fnSetValue) {
         const pxr::VtValue value = fnGetValue(pxr::HdTokens->params);
         hvt::SSAOTaskParams params = value.Get<hvt::SSAOTaskParams>();
         params.ao = app.ao;

         auto renderParams                = sceneFramePass->params().renderParams;
         params.view.cameraID             = renderParams.camera;
         params.view.framing              = renderParams.framing;
         params.view.overrideWindowPolicy = renderParams.overrideWindowPolicy;

         params.ao.isEnabled         = true;
         params.ao.isShowOnlyEnabled = true;
         params.ao.amount            = 2.0f;
         params.ao.sampleRadius      = 10.0f;

         fnSetValue(pxr::HdTokens->params, pxr::VtValue(params));
     };

     // Adds the ssao task i.e., 'ssaoTask' before the color correction one.

     const pxr::SdfPath colorCorrectionTask = sceneFramePass->GetTaskManager()->GetTaskPath(
         pxr::HdxPrimitiveTokens->colorCorrectionTask);

     const pxr::SdfPath ssaoPath =
         sceneFramePass->GetTaskManager()->AddTask<hvt::SSAOTask>(
             hvt::SSAOTask::GetToken(), fnCommit, colorCorrectionTask,
             hvt::TaskManager::InsertionOrder::insertBefore);

     // Sets the default value.

     hvt::SSAOTaskParams ssaoParams;
     ssaoParams.ao = app.ao;

     auto renderParams                    = sceneFramePass->params().renderParams;
     ssaoParams.view.cameraID             = renderParams.camera;
     ssaoParams.view.framing              = renderParams.framing;
     ssaoParams.view.overrideWindowPolicy = renderParams.overrideWindowPolicy;

     ssaoParams.ao.isEnabled         = true;
     ssaoParams.ao.isShowOnlyEnabled = true;
     ssaoParams.ao.amount            = 2.0f;
     ssaoParams.ao.sampleRadius      = 10.0f;

     sceneFramePass->GetTaskManager()->SetTaskValue(
         ssaoPath, pxr::HdTokens->params, pxr::VtValue(ssaoParams));

How to use the FXAA (Anti-aliasing) task

The example in HowTo06_UseFXAARenderTask.cpp demonstrates how to use the FXAA render task of the Hydra Viewport Toolbox. It implements the "Fast Approximate Anti-aliasing" algorithm, which applies an image wide blur filter to smooth out aliasing effects.

Follow the HowTo02 example to create a frame pass and then add the FXAA task as a custom render task.

// Adds the 'FXAA' custom task to the frame pass.

{
    // Defines the anti-aliasing task update function.

    auto fnCommit =
        [&](hvt::TaskManager::GetTaskValueFn const& fnGetValue,
            hvt::TaskManager::SetTaskValueFn const& fnSetValue) {
            const pxr::VtValue value = fnGetValue(pxr::HdTokens->params);
            hvt::FXAATaskParams params = value.Get<hvt::FXAATaskParams>();
            params.resolution = myApp->fxaaResolution;
            fnSetValue(pxr::HdTokens->params, pxr::VtValue(params));
        };

    // Adds the anti-aliasing task i.e., 'fxaaTask'.

    const pxr::SdfPath colorCorrectionTask =
        sceneFramePass->GetTaskManager()->GetTaskPath("colorCorrectionTask");

    // Note: Inserts the FXAA render task into the task list after color correction.

    const pxr::SdfPath fxaaPath =
        sceneFramePass->GetTaskManager()->AddTask<hvt::FXAATask>(
            pxr::TfToken("fxaaTask"), fnCommit, colorCorrectionTask,
            hvt::TaskManager::InsertionOrder::insertAfter);

    // Sets the default value.

    hvt::FXAATaskParams fxaaParams;
    fxaaParams.resolution = myApp->fxaaResolution;
    sceneFramePass->GetTaskManager()->SetTaskValue(
        fxaaPath, pxr::HdTokens->params, pxr::VtValue(fxaaParams));
}

Note that this task can also be used as a custom render task by other task controllers such as the one in the USD library, as explained in the HowTo04 example.

How to include or exclude prims from a frame pass

This example (refer to HowTo07_UseIncludeExclude.cpp for implementation details) demonstrates how to include or exclude prims from a frame pass.

It takes the HowTo02 example to create the frame pass and then, demonstrates the inclusion or exclusion of geometry prims.

Implementation

The code below creates the default collection used by the frame passes (which by default includes all the prims) and only excludes the geometry prims from the grid.

pxr::HdRprimCollection collection { hvt::FramePassParams().collection };
collection.SetExcludePaths({ gridPath });

The code below creates the default collection used by the frame passes but it only includes geometry prims from the grid.

pxr::HdRprimCollection collection { hvt::FramePassParams().collection };
collection.SetRootPath(gridPath);

The code below gets the parameters from the frame pass and set the new collection.

auto& params = sceneFramePass->params();
params.collection = collection;

How to display the bounding box of a scene

This example (refer to HowTo08_UseBoundingBoxSceneIndex.cpp for implementation details) demonstrates how to use a scene index filter like the 'Bounding box' one.

It takes the HowTo02 example to create a single frame pass and it uses the scene index to add the filter.

From the USD documentation:

It's fairly straightforward to implement a scene index by referring to an input scene index for data access, but then selectively overriding the input scene data. This can be thought of as the lazy programming version of running a transformation on the scene at load time. We call this pattern a scene index filter, and provide a base class for this behavior in HdSingleInputFilteringSceneIndexBase.

Implementation using a scene index filter

Note: When the code accesses to a stage it can create the associated scene index using the method ViewportEngine::CreateUSDSceneIndex().

The code to insert scene indice filters is then:

pxr::HdSceneIndexBaseRefPtr sceneIndex = hvt::ViewportEngine::CreateUSDSceneIndex(stage.stage());
sceneIndex = hvt::BoundingBoxSceneIndex::New(sceneIndex);
renderIndex->RenderIndex()->InsertSceneIndex(sceneIndex, pxr::SdfPath::AbsoluteRootPath());

Implementation using USD DrawMode (OpenUSD asset feature)

The bounding box can also be driven through USD's DrawMode schema (OpenUSD UsdImaging, not an HVT helper). That path inserts an override callback when creating the scene index — see UsdImagingDrawModeSceneIndex in pxr/usdImaging/usdImaging/drawModeSceneIndex.h. The HVT-native filter approach above (hvt::BoundingBoxSceneIndex) is what HowTo08 demonstrates.

How to display the wire frame of a scene

This example (refer to HowTo09_UseWireFrameSceneIndex.cpp for implementation details) demonstrates how to display a wire frame of an arbitrary scene.

The example provides two different ways to display a wire frame. The last case combines different render delegates to use the capabilities of each render delegate.

Implementation using the collection representation

The implementation demonstrates how to display the wire frame of an arbitrary scene using the collection representation. The only difference with the HowTo02 (which creates a single frame pass) is to change the default collection representation of the geometry.

auto& params = sceneFramePass->params();

<...>

// Changes the geometry representation.
params.collection = pxr::HdRprimCollection(
    pxr::HdTokens->geometry, pxr::HdReprSelector(pxr::HdReprTokens->wire));

sceneFramePass->Render();

Note: The HD_REPR_TOKENS define lists all the existing representations supported by USD/Hydra. Refer to pxr/imaging/hd/tokens.h for the details.

Implementation using scene index filters (Storm specific)

The implementation demonstrates how to display the wire frame of an arbitrary scene using scene index filters. Unfortunately, that's specific to the Storm render delegate.

// Step 2 - Adds the 'wireframe' scene index.

sceneIndex = hvt::ViewportEngine::CreateUSDSceneIndex(stage.stage());
sceneIndex = hvt::DisplayStyleOverrideSceneIndex::New(sceneIndex);
sceneIndex = hvt::WireFrameSceneIndex::New(sceneIndex);

renderIndex->RenderIndex()->InsertSceneIndex(sceneIndex, pxr::SdfPath::AbsoluteRootPath());

How to explicitly create the list of tasks

This example (refer to HowTo10_CustomListOfTasks.cpp for implementation details) demonstrates how to create one frame pass using the Storm render delegate.

The code contains three ways to manually create the list of tasks.

How to create the default list of tasks

hvt::FramePassDescriptor frameDesc;
frameDesc.renderIndex = renderIndex->RenderIndex();
frameDesc.uid         = pxr::SdfPath("/sceneFramePass");

// Manually creates the default list of tasks.

framePass = std::make_unique<hvt::FramePass>(frameDesc.uid.GetText());
framePass->Initialize(frameDesc);
framePass->CreateDefaultTasks();

How to externally create the default list of tasks.

hvt::FramePassDescriptor frameDesc;
frameDesc.renderIndex = renderIndex->RenderIndex();
frameDesc.uid         = pxr::SdfPath("/sceneFramePass");

// Manually creates the default list of tasks.

framePass = std::make_unique<hvt::FramePass>(frameDesc.uid.GetText());
framePass->Initialize(frameDesc);

// Note: When the render delegate is Storm, the creation is as below.

const hvt::FramePassParams& params = framePass->params();

const auto getLayerSettings = 
    [&framePass]() -> hvt::BasicLayerParams const* {
    return &framePass->params();
};

hvt::TaskCreation::CreateDefaultTasks(framePass->GetTaskManager(),
    framePass->GetRenderBufferAccessor(), framePass->GetLightingAccessor(),
    framePass->GetSelectionSettingsAccessor(), getLayerSettings, false);

How to externally create the minimal list of tasks

hvt::FramePassDescriptor frameDesc;
frameDesc.renderIndex = renderIndex->RenderIndex();
frameDesc.uid         = pxr::SdfPath("/sceneFramePass");

// Manually creates the minimal list of tasks.

framePass = std::make_unique<hvt::FramePass>(frameDesc.uid.GetText());
framePass->Initialize(frameDesc);

const auto getLayerSettings = 
    [&framePass]() -> hvt::BasicLayerParams const* {
    return &framePass->params();
};

hvt::TaskCreation::CreateMinimalTasks(framePass->GetTaskManager(),
    framePass->GetRenderBufferAccessor(), framePass->GetLightingAccessor(),
    getLayerSettings);

How to use the Sky Dome task

This example (refer to HowTo11_UseSkyDomeTask.cpp for implementation details) demonstrates how to add a Sky Dome render task before all existing default render tasks. An important detail that can easily be overlooked is the necessity to have a valid dome light in the frame pass viewInfo parameter:

pxr::GlfSimpleLight domeLight;
domeLight.SetID(pxr::SdfPath("DomeLight"));
domeLight.SetIsDomeLight(true);
params.viewInfo.lights.push_back(domeLight);

Other than this detail, the Sky Dome task is typically inserted before other render tasks, as it should be the farthest geometry and should be rendered first.

How to use the WBOIT (transparency) task

This example (refer to HowTo19_UseWBOITRenderTask.cpp for implementation details) demonstrates weighted blended order-independent transparency (WBOIT) as an alternative to Hydra's default linked-list OIT.

To enable WBOIT, set TaskCreationOptions::useWbOit = true in the FramePassDescriptor before creating the frame pass. The default task list will then use WbOitRenderTask and WbOitResolveTask instead of the standard OIT tasks.

ℹ️ See docs/wboit.md for design details and limitations.

How to use the outline (selection highlight) tasks

This example (refer to HowTo20_UseOutlineTasks.cpp for implementation details) demonstrates GPU selection outlines using ID-buffer edge detection (OutlinePrimIdsTask, OutlineMaskTask, OutlineOverlayTask).

ℹ️ See docs/outline.md for the full pipeline, shader details, and platform limitations (some tests skip Apple/Metal).

How to use the OutlineManager wrapper (recommended)

This example (refer to HowTo21_UseOutlineManager.cpp for implementation details) demonstrates the recommended high-level path for selection outlines via hvt::Outline::OutlineManager. It wraps the five internal outline tasks, AOV bindings, and commit logic so callers only call Install, SetStyle, and SetInputs.

Prefer HowTo21 over HowTo20 for new integrations unless you need direct control over individual outline tasks.

ℹ️ See docs/outline.md § OutlineManager for API details and test/tests/testOutlineManager.cpp for unit coverage.