Tile UV in an instance shader

Hello.

I’m trying to write a shader that will title the UV by column and row based on the InstanceID withing the shader.

What I have is this so far;

v2f vert(appdata_full v, uint instanceID : SV_InstanceID)
            {
                float4 data = positionBuffer[instanceID];

                //The rotation value needs to be negative to face the correct direction.
                v.vertex = RotateAroundYInDegrees(v.vertex, data.w); //Use the 'w' or 4th value of the float4 as the Y angle for the mesh.

                float3 localPosition = v.vertex.xyz * _Scale; //As a scale factor if we want different sized mesh.
                float3 worldPosition = data.xyz + localPosition;
                float3 worldNormal = v.normal;

                float3 ndotl = saturate(dot(worldNormal, _WorldSpaceLightPos0.xyz));
                float3 ambient = ShadeSH9(float4(worldNormal, 1.0f));
                float3 diffuse = (ndotl * _LightColor0.rgb);

                v2f o;
                o.pos = mul(UNITY_MATRIX_VP, float4(worldPosition, 1.0f));

                //o.uv_MainTex = v.texcoord;
                o.uv_MainTex = v.texcoord.xy * (_MainTex_ST.xy * instanceID) + (_MainTex_ST.zw * instanceID);

                o.ambient = ambient;
                o.diffuse = diffuse;

                TRANSFER_SHADOW(o)
                    return o;
            }

Each instance needs to use its own part of the texture but I can not seem to get this to work. Clearly I’m doing something very wrong.

The end goal is to call DrawMeshInstancedIndirect() and the shader will use a specific row/column of the texture based on the instanceID.

If anyone has any thoughts on this, I’d appreciate hearing it.

thanks

I may not be explaining this correctly so let me try this.

I have a texture atlas that is 1024 * 1024, there are 64 tiles in a 128*128 tile grid. I’m trying to figure out how to get my Shader’s instanceID to move the UV’s of that mesh instance to a title.

instanceID 1 = Tile 1
instanceID 2 = Tile 2

instanceID 64 = Tile 64
instanceID 10456 = (whatever that happens to wrap around to)

Hopefully that makes more sense.

thanks

Assuming the UVs for each mesh stay within the 0.0 to 1.0 range, this should be fairly straight forward.

The UVs need to be scaled to be within the range of one tile, and offset on the x and y.

// _Tiles == float4(8.0, 8.0, 1.0 / 8.0, 1.0 / 8.0);

float2 uvOffset = float2(
  floor(fmod(instanceID, _Tiles.x)),
  floor(fmod(instanceID *_Tiles.z, _Tiles.y))
  );

o.uv_MainTex = (v.texcoord.xy + uvOffset.xy) * _Tiles.zw;

You can certainly use the scale / offset variable (_MainTex_ST) to store the _Tiles.

Thank you.

Although something isn’t quite right as the objects are now all black. Here are the relevant parts of the shader;

Properties
    {
        _MainTex("Albedo (RGB)", 2D) = "white" {}
        _Scale("Scale", Range(0, 10)) = 1 //name ("display name", Range (min, max)) = number
        _Cells("X= Columns, Y=Rows", Vector) = (8, 8, 0)
    }

v2f vert(appdata_full v, uint instanceID : SV_InstanceID)
            {
                float4 data = positionBuffer[instanceID];

                //The rotation value needs to be negative to face the correct direction.
                v.vertex = RotateAroundYInDegrees(v.vertex, data.w); //Use the 'w' or 4th value of the float4 as the Y angle for the mesh.

                float3 localPosition = v.vertex.xyz * _Scale; //As a scale factor if we want different sized meshes.
                float3 worldPosition = data.xyz + localPosition;
                float3 worldNormal = v.normal;

                float3 ndotl = saturate(dot(worldNormal, _WorldSpaceLightPos0.xyz));
                float3 ambient = ShadeSH9(float4(worldNormal, 1.0f));
                float3 diffuse = (ndotl * _LightColor0.rgb);

                v2f o;
                o.pos = mul(UNITY_MATRIX_VP, float4(worldPosition, 1.0f));

                //Offset the tile based on the instance id. Cells defines the number of rows and columns.
                float4 Tiles = float4(_Cells.x, _Cells.y, 1.0 / _Cells.x, 1.0 / _Cells.y);

                float2 uvOffset = float2( floor(fmod(instanceID, Tiles.x)), floor(fmod(instanceID * Tiles.z, Tiles.y)) );
                o.uv_MainTex = (v.texcoord.xy + uvOffset.xy) * Tiles.zw;

                //o.uv_MainTex = v.texcoord;
                o.ambient = ambient;
                o.diffuse = diffuse;

                TRANSFER_SHADOW(o)
                return o;
            }

I’m not sure what would be going wrong here…

thanks

No idea. I certainly don’t see anything particularly obviously wrong.

This shader works for me just having multiple quads in the scene with this material, but I’m not implementing a lot of the stuff you have in yours since I’m not drawing them from script.

Shader "Unlit/Instanced Atlas"
{
    Properties
    {
        [NoScaleOffset] _MainTex ("Texture", 2D) = "white" {}
        _Tiles ("Tiles", Vector) = (8,8,0.125,0.125)
        _Index ("Tile Start Index", Float) = 0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_instancing
         
            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

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

            sampler2D _MainTex;
            float4 _Tiles;
            float _Index;
         
            v2f vert (appdata v)
            {
                v2f o;
                UNITY_SETUP_INSTANCE_ID(v);

                o.vertex = UnityObjectToClipPos(v.vertex);

                float index = max(0.0, _Index);

                #if defined(UNITY_INSTANCING_ENABLED) || defined(UNITY_PROCEDURAL_INSTANCING_ENABLED) || defined(UNITY_STEREO_INSTANCING_ENABLED)
                index += unity_InstanceID;
                #endif

                float2 uvOffset = float2(
                    floor(fmod(index, _Tiles.x)),
                    floor(fmod(index *_Tiles.z, _Tiles.y))
                    );

                o.uv = (v.uv.xy + uvOffset.xy) * _Tiles.zw;
                return o;
            }
         
            fixed4 frag (v2f i) : SV_Target
            {
                return tex2D(_MainTex, i.uv);
            }
            ENDCG
        }
    }
}

edit: Replace UNITY_GET_INSTANCE_ID with unity_InstanceID

Thanks.

I tried using your sample almost verbatim, although I think we’re using different versions of unity so I have instanceID coming as a parameter rather than needing to ask for it directly. But still, I’m not getting the correct results. Things are still stretching weirdly.

thanks

That shader is written for Unity 2017+. Are you using 5.4 or older?

I’m using Unity 2017.3 I’m using this with DrawMeshIndirect() which is how the instanceID is being passed in. It’s just a slightly modified version of the ‘Custom Shader’ shown on the bottom of the documentation page here: Unity - Scripting API: Graphics.DrawMeshInstancedIndirect

Still, even if I force-ably set the index to 2 (rather than letting it use the instanceID) I’m getting weird stretching. Maybe I’m not setting up my scaling correctly?

thanks

Using UNITY_GET_INSTANCE_ID is basically just SV_instanceID on the PC, but will work on consoles as well. Looking at the code I think my shader should actually just use unity_InstanceID instead of UNITY_GET_INSTANCE_ID. But as long as you’re not running on a console or using VR it shouldn’t be substantively different from my shader.

With out knowing more about your own shader, mesh, and texture you’re using I couldn’t tell you why it’s not working properly for you.

These are console games so that has to be taken into account, but still, I’ve found very few shaders that needed to be ‘console-ified’ to work. The logic on your end seems sound but I’m clearly doing something not right.

I’ve put a sample application on my OneDrive if you want to take a look at it:
https://1drv.ms/u/s!Ao_BwPapCMyMt164ci05Pf6VPg17

Firefox doesn’t play well with OneDrive so you might need Chrome or IE

The math seems correct, the UV of the plane should offset 1 title per instanceID with a scale of 0.125 (128/1024) but as you can see, it’s stretching strangely rather than showing the full sold colour title.

The UV layout of the plane is correct because if you set the values in the shader properties to 8,8,1,1 you see the first full tile (sold white, black outline) which is how the UV was set up in the modeller. (blender)

thanks

Not entirely sure why your version is showing the issues it does. If I use the built in quad mesh instead of the plane in your example everything works as I would expect. My own shader works on any mesh I apply it to.

Hmm I wonder if it’s because the quad mesh’s UV starts out the full size of the texture. Where as my mesh’s UV is only one tile.

Yes, that is absolutely the issue. In that case you don’t want to scale the UVs in the shader, only offset them. In my example shader that would be done by removing the parentheses on line 58.
o.uv = i.uv + uvOffset * _Tiles.zw;

Thanks! - yes that change worked exactly as expected. Thanks for your help.