Immediate mode polylines not rendering in VR
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?
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 + submeshinto one GPU-instanced call. The instanced primitives reuse a shared static quad mesh, so they merge (instanceCount > 1) and go out viaCommandBuffer.DrawMeshInstanced.Draw.Polygon/Draw.Polylinebuild a unique mesh per path (PolygonPath.EnsureMeshIsReadyToRender→ShapesMeshGen), so they can never merge. They always end atinstanceCount == 1, which takes the non-instanced branch inShapeDrawCall.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 pathUnder 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.DrawMeshInstancedis 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 normalMeshRenderers 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
DrawMeshInstancedwith 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.AddToCommandBufferneeds no change. The instancedShapeDrawCallconstructor already setsinstanced = true, so it goes toDrawMeshInstancedwith a count of 1, which Unity replicates to both eyes.