Skip to content

Edge Outline Visualization

XRTracker includes a real-time edge outline rendering system that draws silhouette and crease edges on tracked objects. This is useful for AR overlays, debugging, and visual feedback.

The system works with both Universal Render Pipeline (URP) and the built-in render pipeline.

Overview

The edge outline system has three parts:

  1. EdgeOutlineRenderer — extracts edges from meshes and builds a screen-space quad mesh each frame
  2. Rendering backend — either a URP ScriptableRendererFeature or a built-in pipeline CommandBuffer, depending on your project setup
  3. Two shadersHidden/EdgeOutlineOcclusion (depth-only) and Hidden/EdgeOutline (screen-space line quads)

Setup

URP Projects

1. Add the Renderer Feature

Add EdgeOutlineFeature to your URP Renderer Asset:

  1. Select your Universal Renderer Data asset
  2. Click Add Renderer FeatureEdge Outline Feature
  3. Set the Render Pass Event (default: BeforeRenderingTransparents)

2. Add an Outline Component

See Outline Components below.

Built-in Pipeline Projects

1. Add the Camera Component

Add EdgeOutlineBuiltIn to your Main Camera:

  1. Select your Main Camera in the scene
  2. Click Add ComponentEdge Outline Built In

Tip

If you add an outline component (TrackedBodyOutline or SimpleEdgeOutline) to a scene that uses the built-in pipeline, the EdgeOutlineBuiltIn component is automatically added to the Main Camera for you.

2. Add an Outline Component

See Outline Components below.

Outline Components

There are two outline components depending on your use case:

TrackedBodyOutline

For tracked objects. Attach to any GameObject that has a TrackedBody component — it automatically uses the same meshes assigned to the TrackedBody.

GameObject (TrackedBody + TrackedBodyOutline)

No additional configuration needed — the mesh source is inherited from the TrackedBody.

SimpleEdgeOutline

For any mesh, tracked or not. Attach to a GameObject with a MeshFilter.

Setting Description Default
Include Children Also extract edges from child MeshFilters Off

Settings

Both outline components expose the same settings via EdgeOutlineRenderer:

Edge Detection

Setting Description Default
Crease Angle Minimum dihedral angle (degrees) between adjacent faces for an edge to be classified as a crease 60

Three types of edges are always extracted:

  • Silhouette edges — edges where one adjacent face points toward the camera and the other points away. View-dependent, updated every frame.
  • Crease edges — edges where the angle between adjacent face normals exceeds the crease angle threshold. Always visible from any angle.
  • Boundary edges — open mesh edges with only one adjacent face. Always visible when their face is front-facing.

Rendering

Setting Description Default
Show Internal Edges Show edges inside the mesh silhouette (internal geometry). When off, only the outer contour is drawn using a stencil mask. Off
Hide Source Mesh Hide the original mesh renderers (show edges only) Off

Width & Color

Setting Description Default
Edge Width Line width in screen-space pixels 2
Edge Color Line color with alpha Cyan (0, 1, 1, 0.9)

How It Works

Edge Extraction

When the mesh changes (or on first frame), EdgeOutlineRenderer analyzes the mesh topology:

  1. Vertex welding — vertices at the same position are merged (quantized to 0.0001 precision) to find shared edges across sub-meshes and split vertices
  2. Adjacency building — each edge maps to its two adjacent face normals
  3. Classification — edges with face normals diverging beyond the crease angle threshold are marked as crease edges. Edges with 3+ adjacent faces are always crease edges.

For multi-mesh objects (e.g., a TrackedBody with several mesh parts), all meshes are processed into a single edge set with vertex positions transformed into the outline component's local space.

Per-Frame Mesh Rebuild

Each frame, the system determines which edges are visible:

  • Crease edges are always drawn
  • Silhouette edges are drawn when the dot products of the two face normals with the view direction have opposite signs (one face is front-facing, the other back-facing)

Visible edges are emitted as screen-space quads (4 vertices, 2 triangles per edge). The vertex shader expands each quad perpendicular to the edge direction in screen space, producing constant-pixel-width lines regardless of distance.

For silhouette edges, the quad is expanded outward (away from the visible face) to avoid covering the object surface.

This runs as a Burst-compiled IJob using NativeArray buffers — zero GC allocation per frame.

Rendering Passes

Both rendering backends use the same two-pass approach:

  1. Stencil mask pass — draws source mesh geometry to the depth and stencil buffers (ColorMask 0, stencil ref=1). This marks every visible mesh pixel.
  2. Edge pass — draws the quad mesh with ZTest LEqual and alpha blending. When "Show Internal Edges" is off, a stencil test (NotEqual 1) ensures only edges outside the mesh silhouette are drawn — hiding internal geometry. A small depth bias pushes lines slightly in front of the surface to prevent z-fighting.

In URP, EdgeOutlineFeature renders via RenderGraph. In the built-in pipeline, EdgeOutlineBuiltIn renders via a CommandBuffer attached at CameraEvent.AfterForwardOpaque.

Both run for the Main Camera and (in URP) the Scene View camera.

Runtime API

All settings are exposed as properties for runtime control:

var outline = GetComponent<EdgeOutlineRenderer>();

// Appearance
outline.EdgeColor = Color.green;
outline.EdgeWidth = 3f;

// Edge detection
outline.CreaseAngle = 45f;

// Rendering
outline.ShowInternalEdges = false;  // only outer contour
outline.HideSourceMesh = true;

// Force rebuild after mesh change
outline.SetDirty();

Note

Changing CreaseAngle triggers an edge rebuild on the next frame. Changing color, width, or other settings is immediate with no rebuild cost.

Performance

  • Edge extraction runs once (or when SetDirty() is called) — not per frame
  • Per-frame work is a single Burst job that tests edge visibility and writes quad geometry
  • Output buffers are pre-allocated at maximum size — no allocations during rendering
  • Typical cost is sub-millisecond for meshes with a few thousand edges