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:
- EdgeOutlineRenderer — extracts edges from meshes and builds a screen-space quad mesh each frame
- Rendering backend — either a URP
ScriptableRendererFeatureor a built-in pipelineCommandBuffer, depending on your project setup - Two shaders —
Hidden/EdgeOutlineOcclusion(depth-only) andHidden/EdgeOutline(screen-space line quads)
Setup¶
URP Projects¶
1. Add the Renderer Feature¶
Add EdgeOutlineFeature to your URP Renderer Asset:
- Select your Universal Renderer Data asset
- Click Add Renderer Feature → Edge Outline Feature
- 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:
- Select your Main Camera in the scene
- Click Add Component → Edge 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.
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:
- Vertex welding — vertices at the same position are merged (quantized to 0.0001 precision) to find shared edges across sub-meshes and split vertices
- Adjacency building — each edge maps to its two adjacent face normals
- 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:
- Stencil mask pass — draws source mesh geometry to the depth and stencil buffers (
ColorMask 0, stencil ref=1). This marks every visible mesh pixel. - Edge pass — draws the quad mesh with
ZTest LEqualand 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