SV_InstanceID not working in world shader

I am working on a GPU particle system in a VRChat world. My plan is to put the position data into a texture and then use a vertex shader to position the particles – this way the simulation will happen completely on the GPU.

I instance the particles using DrawMeshInstanced:

        VRCGraphics.DrawMeshInstanced(
            particleMesh,
            0, // submesh index
            instanceMaterial,
            instanceMatrices,
            instanceCount,
            propertyBlock,
            UnityEngine.Rendering.ShadowCastingMode.Off,
            false, // receive shadows
            layer
        );

The transform matrices are set to a random position and the vertex shader removes this before applying its own transform. This vertex shader positioning works correctly until I try to access each instance with a unique ID.

This code works. I have commented out the stuff that relies on the instance ID. When it runs I see a single particle moving up and down with a sin wave (this is in fact 1023 particles with the same position):

Shader "Unlit/ParticleRenderShader"
{
    Properties {
        _MainTex ("Position Texture", 2D) = "white" {}
        [HDR] _Color ("Color", Color) = (1,1,1,1)
        _ParticleSize ("Size", Float) = 0.1
    }
    SubShader {
        // Important tags for transparency and instancing support
        Tags { "RenderType"="Transparent" "Queue"="Transparent" "IgnoreProjector"="True" }
        Blend SrcAlpha OneMinusSrcAlpha // Standard transparency blend
        ZWrite Off // Usually off for transparent particles
        Cull Off   // Draw both sides

        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_instancing
            #pragma instancing_options procedural:setup

            #include "UnityCG.cginc"

            sampler2D _MainTex;
            float4 _MainTex_TexelSize;
            float4 _Color;
            //sampler2D _PositionTex;
            //float4 _PositionTex_TexelSize;
            //float _ParticleSize;

            struct appdata {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                //uint instanceID : SV_InstanceID;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
                UNITY_VERTEX_OUTPUT_STEREO
            };

            v2f vert (appdata v) {
                v2f o;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);

                // Get the instance ID
                //uint id = v.instanceID;

                // // Convert 1D ID to 2D UV coordinates for the CRT texture
                // float width = _MainTex_TexelSize.z; // Texture width in pixels
                // uint2 pixelCoords = uint2(id % (uint)width, id / (uint)width);
                
                // // // Offset to center of pixel for accurate sampling
                // float2 uv = (float2(pixelCoords) + 0.5) * _MainTex_TexelSize.xy;

                // // Sample the position CRT
                // float4 posData = tex2Dlod(_MainTex, float4(uv, 0, 0));

                // Transform vertex to world space (maintains mesh shape)
                float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
                
                // Reset object's world position to zero (remove transform position)
                worldPos.xyz -= unity_ObjectToWorld._m03_m13_m23;
                
                // Add offset position
                //worldPos.xyz += posData.xyz * 0.01;
                worldPos.y += sin(_Time.y) * 3.0;
                
                // Transform to clip space
                o.vertex = mul(UNITY_MATRIX_VP, worldPos);
                o.uv = v.uv;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target {
                return fixed4(1.0, 1.0, 1.0, 1.0);
            }
            ENDCG
        }
    }
}

As soon as I uncomment the SV_InstanceID here the code stops working and no particles can be seen - no error in console - tested both in editor and build and test on windows:

        struct appdata {
            float4 vertex : POSITION;
            float2 uv : TEXCOORD0;
            //uint instanceID : SV_InstanceID;
            UNITY_VERTEX_INPUT_INSTANCE_ID
        };

Is SV_InstanceID unsupported by VRChat or is there some extra code I would need to include to make it work?

I have also tried:

uint id = unity_InstanceID;

But that results in a compiler error.

What I need is a unique ID for each instance that I can access in the shader. Is this possible in VRChat?

Btw the positions will be calculated in a separate shader outputting to a custom render texture - the above shader is used just for rendering the particles.

Using Unity 2022.3.22f1 with an nvidia GPU

You’re not supposed to manually define uint instanceID : SV_InstanceID; when you already have UNITY_VERTEX_INPUT_INSTANCE_ID, which is a macro that will define that exact same uint instanceID; field (but also handle shader variant and platform/hardware compilation differences for you).

Then in the vert() pass, you should be able to access v.instanceID; just fine.

Thanks for your help! I got it working. There aren’t many examples of this kind of code so it’s hard to figure out.

I was able to address each particle:

Here’s the fixed code for future googlers:

Shader "Unlit/ParticleRenderShader"

{

    Properties {

        _MainTex ("Position Texture", 2D) = "white" {}

        [HDR] _Color ("Color", Color) = (1,1,1,1)

        _ParticleSize ("Size", Float) = 0.1

    }

    SubShader {

        // Important tags for transparency and instancing support

        Tags { "RenderType"="Transparent" "Queue"="Transparent" "IgnoreProjector"="True" }

        Blend SrcAlpha OneMinusSrcAlpha // Standard transparency blend

        ZWrite Off // Usually off for transparent particles

        Cull Off   // Draw both sides




        Pass {

            CGPROGRAM

            #pragma vertex vert

            #pragma fragment frag

            #pragma multi_compile_instancing

            #pragma instancing_options procedural:setup




            #include "UnityCG.cginc"




            sampler2D _MainTex;

            float4 _MainTex_TexelSize;

            float4 _Color;

            //sampler2D _PositionTex;

            //float4 _PositionTex_TexelSize;

            //float _ParticleSize;




            struct appdata {

                float4 vertex : POSITION;

                float2 uv : TEXCOORD0;

                UNITY_VERTEX_INPUT_INSTANCE_ID

            };




            struct v2f {

                float2 uv : TEXCOORD0;

                float4 vertex : SV_POSITION;

                UNITY_VERTEX_INPUT_INSTANCE_ID

                UNITY_VERTEX_OUTPUT_STEREO

            };




            v2f vert (appdata v) {

                v2f o;

                UNITY_SETUP_INSTANCE_ID(v);

                UNITY_TRANSFER_INSTANCE_ID(v, o);

                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);




                // Get the instance ID

                uint id = v.instanceID;




                // // Convert 1D ID to 2D UV coordinates for the CRT texture

                // float width = _MainTex_TexelSize.z; // Texture width in pixels

                // uint2 pixelCoords = uint2(id % (uint)width, id / (uint)width);

                

                // // // Offset to center of pixel for accurate sampling

                // float2 uv = (float2(pixelCoords) + 0.5) * _MainTex_TexelSize.xy;




                // // Sample the position CRT

                // float4 posData = tex2Dlod(_MainTex, float4(uv, 0, 0));




                // Transform vertex to world space (maintains mesh shape)

                float4 worldPos = mul(unity_ObjectToWorld, v.vertex);

                

                // Reset object's world position to zero (remove transform position)

                worldPos.xyz -= unity_ObjectToWorld._m03_m13_m23;

                

                // Add offset position

                //worldPos.xyz += posData.xyz * 0.01;

                worldPos.y += sin(_Time.y) * 3.0 + sin(id * 0.1) * 2.0; // Example movement based on time and ID




                worldPos.x += id * 0.1; // Spread particles along X axis based on ID

                

                // Transform to clip space

                o.vertex = mul(UNITY_MATRIX_VP, worldPos);

                o.uv = v.uv;

                return o;

            }




            fixed4 frag (v2f i) : SV_Target {

                return fixed4(1.0, 1.0, 1.0, 1.0);

            }

            ENDCG

        }

    }

}
1 Like