Immediate mode polylines not rendering in VR

Avatar
  • updated

I have a project that uses immediate mode polylines in a VR game. I am upgrading my project from Unity 2021.3.45 + Shapes 4.3.1 to Unity 6000.4.2 + Shapes 4.6.0. It still works almost perfectly, except polylines do not render in build on headset (Quest 3)

The immediate mode polylines still render correctly in editor as well as in WebGPU builds. I am able to place regular polylines in scene and see those in headset, so the problem is limited to immediate mode. No errors or anything at runtime. I've been messing around with settings and cannot for the life of me figure out why this is happening. Any ideas?

Reporting a bug? please specify Unity version:
6000.4.2
Reporting a bug? please specify Shapes version:
4.6.0
Reporting a bug? please specify Render Pipeline:
URP
Avatar
Hendrik Schulte

I stumbled upon the exact same thing (Shapes 4.6.0, URP 17, Unity 6.3, Quest 3, OpenXR single-pass) which not only applies to Draw.Polyline but also Draw.Polygon. This is what Claude came up fixing the problem for me:

Root cause

Shapes batches consecutive immediate draws that share mesh + material + submesh into one GPU-instanced call. The instanced primitives reuse a shared static quad mesh, so they merge (instanceCount > 1) and go out via CommandBuffer.DrawMeshInstanced.

Draw.Polygon / Draw.Polyline build a unique mesh per path (PolygonPath.EnsureMeshIsReadyToRenderShapesMeshGen), so they can never merge. They always end at instanceCount == 1, which takes the non-instanced branch in ShapeDrawCall.AddToCommandBuffer (ShapeDrawState.cs):

if( instanced )
    cmd.DrawMeshInstanced( drawState.mesh, drawState.submesh, drawState.mat, 0, matrices, count, mpb );
else
    cmd.DrawMesh( drawState.mesh, matrix, drawState.mat, drawState.submesh, 0, mpb );  // <-- single-mesh path

Under single-pass stereo, CommandBuffer.DrawMesh (non-instanced) doesn't get the eye replication that instancing provides, so it renders to a single eye slice / effectively drops. CommandBuffer.DrawMeshInstanced is what carries the draw to both eyes. That's why the instanced primitives are fine and the unique-mesh ones (polygon, polyline) are not — and why scene-component versions work: they render as normal MeshRenderers through URP's own path, which sets up stereo correctly.

This also fits the regression from Unity 2021 + Shapes 4.3.1 to Unity 6 + 4.6.0: the Unity 6 RenderGraph path routes through RasterCommandBuffer.DrawMesh, which shows the same non-instanced single-eye behavior.

Fix

Route the single-instance (non-batched) draw through DrawMeshInstanced with a length-1 matrix array when XR is active. The instanced shader variant reads per-shape properties from the instancing constant buffer (UNITY_ACCESS_INSTANCED_PROP), so the MPB has to be populated as arrays on this path too — the "instanced vs not" and "array vs scalar MPB" decisions have to flip together, so we drive both from one predicate.

MetaMpb.cs:

// new members
bool forceInstanced = false;
// Under single-pass stereo a non-instanced CommandBuffer.DrawMesh reaches only one eye;
// unique-mesh draws (Polygon/Polyline) can't batch, so force them through the instanced path.
bool ForceInstancedForStereo => UnityEngine.XR.XRSettings.enabled && this is MpbText == false;
bool UsingInstancedDraw => HasMultipleInstances || forceInstanced;

public ShapeDrawCall ExtractDrawCall() {
    bool useOverrideMpb = mpbOverride != null && this is MpbCustomMesh;
    forceInstanced = ForceInstancedForStereo;                 // cache once

    if( UsingInstancedDraw ) {                                // was: HasMultipleInstances
        sdc = new ShapeDrawCall( drawState, instanceCount, matrices, useOverrideMpb ? mpbOverride : null );
        matrices = ArrayPool<matrix4x4>.Alloc( UnityInfo.INSTANCES_MAX );
    } else
        sdc = new ShapeDrawCall( drawState, matrices[0], useOverrideMpb ? mpbOverride : null );

    if( useOverrideMpb == false )
        TransferAllProperties();
    Dispose();
    return sdc;
}

// in BOTH Transfer(...) overloads, replace `HasMultipleInstances` with `UsingInstancedDraw`
// so the MPB is written with SetVectorArray / SetFloatArray on this path:
protected void Transfer( int propertyID, List<vector4> listVec ) {
    if( directMaterialApply )
        drawState.mat.SetVector( propertyID, listVec[0] );
    else if( UsingInstancedDraw )                             // was: HasMultipleInstances
        sdc.mpb.SetVectorArray( propertyID, listVec );
    else
        sdc.mpb.SetVector( propertyID, listVec[0] );
    listVec.Clear();
}
// ...same swap for the List<float> overload...

public void Dispose() {
    initialized = false;
    drawState = default;
    instanceCount = 0;
    forceInstanced = false;
}

ShapeDrawCall.AddToCommandBuffer needs no change. The instanced ShapeDrawCall constructor already sets instanced = true, so it goes to DrawMeshInstanced with a count of 1, which Unity replicates to both eyes.

Avatar
Thomas Ratliff
Quote from Freya Holmér

that is odd. was this with a clean install of Shapes (ie: installed 4.6.0 fresh instead of upgrading it in place), and are you getting any potentially related shader warnings or errors in the build log or at runtime?

Ya, this was on a clean install. I made sure to fully delete the package and reinstall the new one. No errors/warnings during build or runtime which is why I'm having such a hard time debugging what could be going wrong here

Avatar
Freya Holmér creator

that is odd. was this with a clean install of Shapes (ie: installed 4.6.0 fresh instead of upgrading it in place), and are you getting any potentially related shader warnings or errors in the build log or at runtime?