Ashton Way
Ashton Way

Reputation: 56

GPU Instancing shader has InstanceID in the wrong order

I am at my wits end with this one. I have been attempting to put together a script that would let me render many instances of a given mesh and material with specific transforms relative to a GameObject. I have been using the Graphics.RenderMeshInstanced() method to achieve this in this somewhat janky script:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class InstancedRenderer : MonoBehaviour
{
    public Mesh mesh;
    public Material sharedMaterial;

    private Transform tsfm;

    public Material material { get; private set; }
    private Matrix4x4[] matrices;

    private List<ComputeBuffer> buffers;

    private void Awake()
    {
        tsfm = transform;

        if(sharedMaterial != null)
            SetMaterial(sharedMaterial);
    }

    private void Update()
    {
        if(tsfm.hasChanged)
            material.SetMatrix("transform", transform.localToWorldMatrix);

        RenderMeshes();
    }

    private void RenderMeshes()
    {
        if (matrices == null)
            return;

        RenderParams parameters = new RenderParams()
        {
            camera = null,
            layer = 0,
            material = this.material,
            receiveShadows = true,
            worldBounds = new Bounds(transform.position, Vector3.one * 100),
            
            shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.On,
        };

        material.SetInteger("instanceCount", matrices.Length);

        Graphics.RenderMeshInstanced(parameters, mesh, 0, matrices);
    }

    public void SetMaterial(Material mat)
    {
        sharedMaterial = mat;
        material = Instantiate(mat);
    }

    public void SetTransforms(Matrix4x4[] transforms)
    {
        matrices = transforms;
    }

    public void ApplyBuffer(string name, ComputeBuffer buffer)
    {
        material.SetBuffer(name, buffer);

        if (buffers == null)
            buffers = new List<ComputeBuffer>();

        buffers.Add(buffer);
    }

    private void OnDestroy()
    {
        foreach (ComputeBuffer buffer in buffers)
        {
            buffer.Release();
        }
    }
}

In the vertex shader I use these lines

            v.vertex = mul(unity_WorldToObject, mul(transform, mul(unity_ObjectToWorld, v.vertex)));
            float3 normal = normalize(mul(unity_WorldToObject, mul(transform, mul(unity_ObjectToWorld, float3(v.normal.xyz)))) );
            v.normal = normal;

To make the rendered meshes follow the game object (transform is a float4x4)

I am doing this as a way to performantly render large amounts of grass in my low poly game and it worked for the most part. Before this I had been combining all the instances into one mesh and rendering that as a single object which was better than having individual objects but generating truly strange glitches and artifacts.

The problem shows itself when I try to apply a unique color to each grass instance. I am passing the colors in a structured buffer of float4s and accessing it through the instance id in the vertex shader. This seems to work at first but the colors appear to be in the complete wrong order.Image

The grass color should match the terrain color. I have scoured the internet for anything that could cause this. I have even reverted to the less performant one mesh implementation to double check that I am generating the array of colors in the same order as the array of matrices but I cant see anything wrong. I really don't know what I am doing here and any insight at all would be greatly appreciated. I would be open to ideas on how to make it work or other things I could replace this whole system with.

Additionally, this is the rest of the grass shader, though it is messy enough that it desperately needs to be re-written from scratch.

Shader "Custom/InstancedGrass"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        #pragma multi_compile_instancing
        #pragma surface surf Lambert vertex:vert addshadow

        // Use shader model 3.0 target, to get nicer looking lighting
        #pragma target 5.0

        #ifdef SHADER_API_D3D11
            StructuredBuffer<fixed4> _TestColors;
        #endif

        sampler2D _MainTex;

        int instanceCount;
        float4x4 transform;

        struct appdata
        {
            float4 vertex : POSITION;
            float3 normal : Normal;
            UNITY_VERTEX_INPUT_INSTANCE_ID
        };

        struct Input
        {
            float2 uv_MainTex : TEXCOORD0;

            float4 instanceColor;
        };


        void vert(inout appdata v, out Input o)
        {
            UNITY_SETUP_INSTANCE_ID(v);

            v.vertex = mul(unity_WorldToObject, mul(transform, mul(unity_ObjectToWorld, v.vertex)));
            float3 normal = normalize(mul(unity_WorldToObject, mul(transform, mul(unity_ObjectToWorld, float3(v.normal.xyz)))) );
            v.normal = normal;
            
            UNITY_INITIALIZE_OUTPUT(Input, o);

            #ifdef UNITY_INSTANCING_ENABLED

                uint id = UNITY_GET_INSTANCE_ID(v);
                o.instanceColor = _TestColors[id];
            #endif
        }

        fixed4 _Color;

        // Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
        // See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
        // #pragma instancing_options assumeuniformscaling
        UNITY_INSTANCING_BUFFER_START(Props)

        UNITY_INSTANCING_BUFFER_END(Props)

        void surf (Input IN, inout SurfaceOutput o)
        {
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;

            fixed4 testColor = IN.instanceColor;

            o.Albedo = testColor;
            o.Alpha = 1;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

Upvotes: 1

Views: 291

Answers (1)

Axel Stewart
Axel Stewart

Reputation: 118

I don't know the cause of this behavior, but I do have good news about RenderMeshInstanced.

You are currently passing an array of Matrix4x4s for the InstanceData, but the function accepts any struct as long as you give it an objectToWorld matrix. You can pass it an array of structs containing the transform and your float4 (Vector4) color. This will ensure that your data travels together.

If this isn't enough to go on, I could potentially spin up a project and look into this. I'm still a little foggy on how the custom InstanceData members are supposed to be accessed within the shader, but I would be happy to take a crack at it if you're still stuck.

Upvotes: 1

Related Questions