DrawProcedualで1マテでスキニングモデルの一括描画テスト

環境

Unity2021.2.18f1

burst1.6.5

jobs0.50.0-preview.8

概要

DrawProcedualの実験です。 多数のスキニングモデルを1マテリアルで一括描画してみるテストです。

AnimationInstancingのように1つのモデルを大量に出すのではなく、複数種類のモデルをたくさん出す為のテストです。

面倒だったので使用するスキニングモデルは1スキンモデルのみにしています。

アニメーション毎にボーン行列をGraphicsBufferに保存して、シェーダでスキニング処理をしています。

テクスチャはTexture2DArrayに纏めています。(モデルのテクスチャはベーステクスチャのみで全部同じサイズ)

また、無駄にComputeShaderやJobSystemを使用しています。

左上の数値はFPSです。オブジェクトは全部で1000体出しています。

※モデルはオプティマイズオプションを無効にしないと実機でちゃんと表示されませんでした。

ModelManager.cs

using System;
using System.Linq;
using System.Collections.Generic;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine;
using Unity.Jobs;
using Unity.Burst;

public class ModelManager: MonoBehaviour
{
    [SerializeField] private ComputeShader _computeShader = null;
    [SerializeField] private Material _material = null;
    [SerializeField] private GameObject[] _prefabs = null;
    private class ModelCreateParam
    {
        //Vertices
        public int vertexOffset = 0;
        public GraphicsBuffer[] gbVertexStreams;
        public int vertexCount = 0;
        //Indices
        public int indexOffset = 0;
        public GraphicsBuffer gbIndices;
        //Textures
        public int textureOffset = 0;
        public Texture[] textures = null;
        //Animations
        public int animationClipOffset = 0;
        public int boneCount = 0;
        public AnimationClipCreateParam[] animationClipCreateParams = null;
    }
    private List<ModelCreateParam> _modelCreateParams = new List<ModelCreateParam>();
    private struct ModelParam
    {
        public int vertexOffset;
        public int indexOffset;
        public int textureOffset;
        public int animationClipOffset;
        public int boneCount;
    }
    private NativeArray<ModelParam> _modelParams;
    private int _vertexStride = 0;
    private GraphicsBuffer _gbVertices = null;

    private class AnimationClipCreateParam
    {
        public int nameHash = 0;
        public int frameCount = 0;
        public int boneMatrixOffset = 0;
        public Matrix4x4[] boneMatrices = null;
    }
    private struct AnimationClipParam
    {
        public int nameHash;
        public int frameCount;
        public int boneMatrixOffset;
    }
    NativeArray<AnimationClipParam> _animationClipParams;

    private GraphicsBuffer _gbBoneMatrices = null;

    private class TextureParam
    {
        public int spTextureId;
        public Texture2DArray textureArray = null;
    }
    private static TextureParam[] _textureParams = 
    {
        new TextureParam(){spTextureId = Shader.PropertyToID("_MainTex") },
    };
    private static int _spSrcVertices = Shader.PropertyToID("SrcVertices");
    private static int _spSrcVertexCount = Shader.PropertyToID("SrcVertexCount");
    private static int _spSrcStride = Shader.PropertyToID("SrcStride");
    private static int _spDestVertices = Shader.PropertyToID("DestVertices");
    private static int _spDestStride = Shader.PropertyToID("DestStride");
    private static int _spDestOffsetVertex = Shader.PropertyToID("DestOffsetVertex");
    private static int _spDestOffsetInStride = Shader.PropertyToID("DestOffsetInStride");

    private static int _spDestIndices = Shader.PropertyToID("DestIndices");
    private static int _spDestOffsetIndex = Shader.PropertyToID("DestOffsetIndex");
    private static int _spSrcIndexCount = Shader.PropertyToID("SrcIndexCount");
    private static int _spSrcUnitIndex = Shader.PropertyToID("SrcUnitIndex");
    private static int _spSrcVertexOffset = Shader.PropertyToID("SrcVertexOffset");
    
    private static int _spSrcIndices = Shader.PropertyToID("SrcIndices");

    private static int _spVerticesId = Shader.PropertyToID("_Vertices");
    private static int _spBoneMatricesId = Shader.PropertyToID("_BoneMatrices");
    private static int _spUnitDescsId = Shader.PropertyToID("_UnitDescs");
    private static int _spUnitUpdateDescsId = Shader.PropertyToID("_UnitUpdateDescs");
    private bool _modelCreateParamDirty = false;

    [BurstCompile(CompileSynchronously = true)]
    private struct UnitParam
    {
        public int modelId;
        public int animationId;
        public Vector3 position;
        public Quaternion rotation;
        public Vector3 scale;
        public float animationTime;

        private enum State
        {
            Wait,
            Walk,
        }
        private State state;
        private Vector3 basePosition;
        private float waitTime;
        private Vector3 destPosition;
        private Unity.Mathematics.Random random;

        public void Setup()
        {
            random = new Unity.Mathematics.Random((uint)UnityEngine.Random.Range(1, 65535));
            basePosition = position;
            state = State.Wait;
            waitTime = UnityEngine.Random.Range(0.0f, 3.0f);
        }

        [BurstCompile(CompileSynchronously = true)]
        public unsafe void Update(float deltaTime)
        {
            if(state == State.Wait)
            {
                if(waitTime > 0)
                {
                    waitTime -= deltaTime;
                    return;
                }
                state = State.Walk;
                var q = Quaternion.Euler(0.0f, random.NextFloat(0, 360), 0.0f);
                var v = q * new Vector3(0, 0, 10);
                destPosition = basePosition + v;
            }
            else if(state == State.Walk)
            {
                var v = destPosition - position;
                const float minDist = 0.5f;
                if(v.sqrMagnitude < minDist * minDist)
                {
                    state = State.Wait;
                    waitTime = 1.0f;
                    return;
                }
                var destRot = Quaternion.LookRotation(v);
                const float rotSpeed = 3.0f;
                rotation = Quaternion.SlerpUnclamped(rotation, destRot, rotSpeed * deltaTime);
                var forward = rotation * Vector3.forward;

                const float moveSpeed = 1.0f;
                position += forward * moveSpeed * deltaTime;
            }
        }
    }
    private NativeList<UnitParam> _units;
    [BurstCompile(CompileSynchronously = true)]
    struct JobUnits : IJobParallelFor
    {
        public NativeArray<UnitParam> units;
        public NativeArray<UnitUpdateDesc> unitUpdateDescs;
        [ReadOnly] public NativeArray<ModelParam> modelParams;
        [ReadOnly] public NativeArray<AnimationClipParam> animationClipParams;
        [ReadOnly] public float deltaTime;

        public unsafe void Execute(int index)
        {
            var pUnits = (UnitParam*)NativeArrayUnsafeUtility.GetUnsafePtr(units);
            var pUnit = &pUnits[index];
            pUnit->Update(deltaTime);
            UpdateUnitDesc(pUnit, index);
        }

        private unsafe void UpdateUnitDesc(UnitParam* pUnit, int index)
        {
            var pUnitUpdateDesc = (UnitUpdateDesc*)NativeArrayUnsafeUtility.GetUnsafePtr(unitUpdateDescs);
            var modelParam = modelParams[pUnit->modelId];
            var animParam =  animationClipParams[modelParam.animationClipOffset + pUnit->animationId];
            var animFrame = (int)(pUnit->animationTime * _animationFrameRate);
            pUnit->animationTime = Mathf.Repeat(pUnit->animationTime + deltaTime, (float)animParam.frameCount / (float)_animationFrameRate);
            pUnitUpdateDesc[index].boneMatrixId = animParam.boneMatrixOffset + animFrame * modelParam.boneCount;
            pUnitUpdateDesc[index].worldMatrix = Matrix4x4.TRS(pUnit->position, pUnit->rotation, pUnit->scale);
        }
    }
    private JobHandle _jhUnits;

    private bool _unitDirty = false;
    private GraphicsBuffer _gbIndices = null;
    private GraphicsBuffer _gbArgs = null;

    private struct UnitDesc
    {
        public int modelId;//textureId
    }
    private GraphicsBuffer _gbUnitDescs = null;
    private struct UnitUpdateDesc
    {
        public Matrix4x4 worldMatrix;
        public int boneMatrixId;
    }
    private NativeArray<UnitUpdateDesc> _unitUpdateDescs;
    private GraphicsBuffer _gbUnitUpdateDescs = null;
    private Bounds _bounds;
    private const int _animationFrameRate = 30;

    private void Start()
    {
        for(int i = 0; i < _prefabs.Length; i++)
        {
            CreateModelCreateParam(_prefabs[i]);
        }
        CreateTextureArrays();
        _units = new NativeList<UnitParam>(Allocator.Persistent);
        for(int i = 0; i < 1000; i++)
        {
            var x = (i % 25 - 12) * 2.0f;
            var z = (i / 25 - 12) * 2.0f;
            var modelId = i %  _modelCreateParams.Count;
            var animationId = Animator.StringToHash("idle");
            CreateUnit(modelId, animationId, new Vector3(x, 0.0f, z), Quaternion.identity, Vector3.one);
        }
        _bounds = new Bounds(Vector3.zero, new Vector3(1000.0f, 1000.0f, 1000.0f));// mesh.bounds;
    }

    private void OnDestroy()
    {
        _jhUnits.Complete();

        for(int i = 0; i < _modelCreateParams.Count; i++)
        {
            DestroyModelCreateParam(_modelCreateParams[i]);
        }
        if(_gbVertices != null)
            _gbVertices.Dispose();
        if(_gbIndices != null)
            _gbIndices.Dispose();
        if(_gbArgs != null)
            _gbArgs.Dispose();
        if(_gbBoneMatrices != null)
            _gbBoneMatrices.Dispose();
        if(_gbUnitDescs != null)
            _gbUnitDescs.Dispose();
        if(_gbUnitUpdateDescs != null)
            _gbUnitUpdateDescs.Dispose();
        if(_unitUpdateDescs.IsCreated == true)
            _unitUpdateDescs.Dispose();
        if(_units.IsCreated == true)
            _units.Dispose();
        if(_modelParams.IsCreated == true)
            _modelParams.Dispose();
        if(_animationClipParams.IsCreated == true)
            _animationClipParams.Dispose();
    }

    private unsafe void CreateModelCreateParam(GameObject prefab)
    {
        _modelCreateParamDirty = true;
        var vertexOffset = 0;
        var indexOffset = 0;
        var animationClipOffset = 0;
        if(_modelCreateParams.Count > 0)
        {
            var lastModelCreateParam = _modelCreateParams[_modelCreateParams.Count - 1];
            vertexOffset = lastModelCreateParam.vertexOffset + lastModelCreateParam.vertexCount;
            indexOffset = lastModelCreateParam.indexOffset + lastModelCreateParam.gbIndices.count;
            animationClipOffset = lastModelCreateParam.animationClipOffset + lastModelCreateParam.animationClipCreateParams.Length;
        }

        Mesh mesh;
        Renderer renderer;
        var skinnedMeshRenrerer = prefab.GetComponentInChildren<SkinnedMeshRenderer>();
        if(skinnedMeshRenrerer != null)
        {
            mesh = skinnedMeshRenrerer.sharedMesh;
            renderer = skinnedMeshRenrerer;
        }
        else
        {
            var meshFilter = prefab.GetComponentInChildren<MeshFilter>();
            mesh = meshFilter.sharedMesh;
            renderer = meshFilter.GetComponent<Renderer>();
        }
        var vertexStreamNum = 0;
        var attrs = mesh.GetVertexAttributes();
        vertexStreamNum = attrs[attrs.Length - 1].stream + 1;
        var gbVertexStreams = new GraphicsBuffer[vertexStreamNum];
        for(int i = 0; i < vertexStreamNum; i++)
        {
            gbVertexStreams[i] = mesh.GetVertexBuffer(i);
            if((gbVertexStreams[i].target & GraphicsBuffer.Target.Raw) == 0)
            {
                var count = gbVertexStreams[i].count;
                var stride = gbVertexStreams[i].stride;
                var temp = new byte[count, stride];
                gbVertexStreams[i].GetData(temp);
                gbVertexStreams[i].Dispose();
                gbVertexStreams[i] = new GraphicsBuffer(GraphicsBuffer.Target.Vertex | GraphicsBuffer.Target.Raw, count, stride);
                gbVertexStreams[i].SetData(temp);
            }
        }

        var vertexCount = gbVertexStreams[0].count;
        var vertexStride = 0;
        for(int i = 0; i < vertexStreamNum; i++)
            vertexStride += gbVertexStreams[i].stride;
        if(_vertexStride == 0)
            _vertexStride = vertexStride;
        if(_vertexStride != vertexStride)
            Debug.LogError($"_vertexStride != vertexStride prefab:{prefab.name}");
        var gbIndices = mesh.GetIndexBuffer();
        var sourceIndices = new UInt16[gbIndices.count];
        gbIndices.GetData(sourceIndices);
        fixed(UInt16* pSrcIndices = sourceIndices)
        using(var destIndices = new NativeArray<UInt32>(gbIndices.count, Allocator.Temp))
        {
            var pDestIndices = (UInt32*)NativeArrayUnsafeUtility.GetUnsafePtr(destIndices);
#if true
            UnsafeUtility.MemCpyStride(pDestIndices, sizeof(UInt32), pSrcIndices, sizeof(UInt16), sizeof(UInt16), gbIndices.count);
#else
            for(int i = 0; i< sourceIndices.Length; i++)
                pDestIndices[i] = /*(UInt32)vertexOffset + */(UInt32)pSrcIndices[i];
#endif
            gbIndices.Dispose();
            gbIndices = new GraphicsBuffer(GraphicsBuffer.Target.Index | GraphicsBuffer.Target.Raw, destIndices.Length, sizeof(UInt32));
            gbIndices.SetData(destIndices);
        }

        var material = renderer.sharedMaterial;
        var textures = new Texture[_textureParams.Length];
        for(int  i = 0; i < _textureParams.Length; i++)
        {
            var textureParam = _textureParams[i];
            textures[i] = material.GetTexture(textureParam.spTextureId);
        }

        var modelCreateParam = new ModelCreateParam();
        modelCreateParam.vertexOffset = vertexOffset;
        modelCreateParam.gbVertexStreams = gbVertexStreams;
        modelCreateParam.vertexCount = vertexCount;
        modelCreateParam.indexOffset = indexOffset;
        modelCreateParam.gbIndices = gbIndices;
        modelCreateParam.textureOffset = _modelCreateParams.Count - 1;
        modelCreateParam.textures = textures;
        modelCreateParam.animationClipOffset = animationClipOffset;
        modelCreateParam.boneCount = skinnedMeshRenrerer.bones.Length;
        if(skinnedMeshRenrerer != null)
            CreateAnimations(modelCreateParam, prefab, skinnedMeshRenrerer);

        _modelCreateParams.Add(modelCreateParam);
    }

    private void CreateAnimations(ModelCreateParam ModelCreateParam, GameObject prefab, SkinnedMeshRenderer skinnedMeshRenderer)
    {
        var boneMatrixOffset = 0;
        if(_modelCreateParams.Count > 0)
        {
            var lastModelCreateParam = _modelCreateParams[_modelCreateParams.Count - 1];
            var lastAnimationParam = lastModelCreateParam.animationClipCreateParams[lastModelCreateParam.animationClipCreateParams.Length - 1];
            boneMatrixOffset = lastAnimationParam.boneMatrixOffset + lastAnimationParam.boneMatrices.Length;
        }

        var animParams = new List<AnimationClipCreateParam>();
        var animator = prefab.GetComponentInChildren<Animator>();
        var animClips = animator.runtimeAnimatorController.animationClips;
        ModelCreateParam.animationClipCreateParams = new AnimationClipCreateParam[animClips.Length];

        var prevPrefabPos = prefab.transform.localPosition;
        var prevPrefabRot = prefab.transform.localRotation;
        var prevPrefabScale = prefab.transform.localScale; 
        prefab.transform.position = Vector3.zero;
        prefab.transform.rotation = Quaternion.identity;
        for(int i = 0; i < animClips.Length; i++)
        {
            var animParam = new AnimationClipCreateParam();
            var animClip = animClips[i];
            animParam.nameHash = Animator.StringToHash(animClip.name.ToLower());
            animParam.boneMatrixOffset = boneMatrixOffset;
            animParam.frameCount = (int)(animClip.length * _animationFrameRate);
            animParam.boneMatrices = new Matrix4x4[animParam.frameCount * skinnedMeshRenderer.bones.Length];
            var matrixIndex = 0;
            for(int frame = 0; frame < animParam.frameCount; frame++)
            {
                animClip.SampleAnimation(prefab, (float)frame / _animationFrameRate);
                for(int boneIndex = 0; boneIndex < skinnedMeshRenderer.bones.Length; boneIndex++)
                {
                    var bone = skinnedMeshRenderer.bones[boneIndex];
                    animParam.boneMatrices[matrixIndex] = bone.localToWorldMatrix * skinnedMeshRenderer.sharedMesh.bindposes[boneIndex];
                    matrixIndex++;
                }
            }
            boneMatrixOffset += animParam.boneMatrices.Length;
            ModelCreateParam.animationClipCreateParams[i] = animParam;
        }
        prefab.transform.localPosition = prevPrefabPos;
        prefab.transform.localRotation = prevPrefabRot;
        prefab.transform.localScale = prevPrefabScale; 
        animClips[0].SampleAnimation(prefab, (float)0.0f);
    }

    private void DestroyModelCreateParam(ModelCreateParam ModelCreateParam)
    {
        for(int i = 0; i < ModelCreateParam.gbVertexStreams.Length; i++)
        {
            if(ModelCreateParam.gbVertexStreams[i] != null)
                ModelCreateParam.gbVertexStreams[i].Dispose();
        }
        if(ModelCreateParam.gbIndices != null)
            ModelCreateParam.gbIndices.Dispose();
    }

    private void CreateTextureArrays()
    {
        if(_modelCreateParamDirty == false)
            return;
        var baseModelCreateParam = _modelCreateParams[0];
        for(int i = 0; i < _textureParams.Length; i++)
        {
            var textureParam = _textureParams[i];
            var baseTexture = baseModelCreateParam.textures[i] as Texture2D;
            textureParam.textureArray = new Texture2DArray(baseTexture.width, baseTexture.height,  _modelCreateParams.Count, baseTexture.format, baseTexture.mipmapCount, false);
            for(int j = 0; j < _modelCreateParams.Count; j++)
            {
                var modelCreateParam = _modelCreateParams[j];
                var srcTex = modelCreateParam.textures[i] as Texture2D;
                for(int m = 0; m < srcTex.mipmapCount; m++)
                {
                    var pixelData = srcTex.GetPixelData<Color32>(m);
                    textureParam.textureArray.SetPixelData<Color32>(pixelData, m, j);
                }
            }
            textureParam.textureArray.Apply(updateMipmaps: false);
        }
    }

    public int CreateUnit(int modelId, int animationNameToHash, Vector3 position, Quaternion rotation, Vector3 scale)
    {
        var unit = new UnitParam();
        unit.modelId = modelId;
        unit.animationId = _modelCreateParams[unit.modelId].animationClipCreateParams.Select((x,i) => (x, i )).Where(y => y.x.nameHash == animationNameToHash).Select(z => z.i).FirstOrDefault();
        unit.position = position;
        unit.rotation = rotation;
        unit.scale = scale;
        unit.animationTime = 0.0f;
        unit.Setup();
        _units.Add(unit);
        _unitDirty = true;
        return _units.Length - 1;
    }

    private unsafe void LateUpdate()
    {
        if(_unitUpdateDescs.IsCreated == true)
        {
            _jhUnits.Complete();
            _gbUnitUpdateDescs.SetData(_unitUpdateDescs);
            if(_gbArgs != null)
                Graphics.DrawProceduralIndirect(_material, _bounds, MeshTopology.Triangles, _gbIndices, _gbArgs);
        }

        var unitCount = _units.Length;
        if(_modelCreateParamDirty == true)
        {
            _modelCreateParamDirty = false;
            //Vertices
            if(_gbVertices != null)
                _gbVertices.Dispose();
            var lastModelCreateParam = _modelCreateParams[_modelCreateParams.Count - 1];
            var vertexCount = lastModelCreateParam.vertexOffset + lastModelCreateParam.vertexCount;
            _gbVertices = new GraphicsBuffer(GraphicsBuffer.Target.Vertex | GraphicsBuffer.Target.Raw, (int)vertexCount,  _vertexStride);
            var kernelIndex = _computeShader.FindKernel("GenerateVertices");
            uint numThreadX, numThreadY, numThreadZ;
            _computeShader.GetKernelThreadGroupSizes(kernelIndex, out numThreadX, out numThreadY, out numThreadZ);
            _computeShader.SetBuffer(kernelIndex, _spDestVertices, _gbVertices);
            _computeShader.SetInt(_spDestStride, _vertexStride);
            for(int i = 0; i < _modelCreateParams.Count; i++)
            {
                var modelCreateParam = _modelCreateParams[i];
                _computeShader.SetInt(_spDestOffsetVertex, modelCreateParam.vertexOffset * _vertexStride);
                _computeShader.SetInt(_spSrcVertexCount, modelCreateParam.vertexCount);
                var groupLoopCount = (modelCreateParam.vertexCount + (int)(numThreadY - 1)) / (int)numThreadY;
                var destOffsetInStride = 0;
                for(int j = 0; j < modelCreateParam.gbVertexStreams.Length; j++)
                {
                    var srcStride = modelCreateParam.gbVertexStreams[j].stride;
                    _computeShader.SetBuffer(kernelIndex, _spSrcVertices, modelCreateParam.gbVertexStreams[j]);
                    _computeShader.SetInt(_spSrcStride, srcStride);
                    _computeShader.SetInt(_spDestOffsetInStride, destOffsetInStride);
                    _computeShader.Dispatch(kernelIndex, srcStride / 4, groupLoopCount, 1);
                    destOffsetInStride += srcStride;
                }
            }
            _material.SetBuffer(_spVerticesId, _gbVertices);
            //ModelParams
            if(_modelParams.IsCreated == true)
                _modelParams.Dispose();
            _modelParams = new NativeArray<ModelParam>(_modelCreateParams.Count, Allocator.Persistent);
            var pModelParams = (ModelParam*)NativeArrayUnsafeUtility.GetUnsafePtr(_modelParams);
            for(int i = 0; i < _modelCreateParams.Count; i++)
            {
                var modelCreateParam = _modelCreateParams[i];
                pModelParams[i].vertexOffset = modelCreateParam.vertexOffset;
                pModelParams[i].indexOffset = modelCreateParam.indexOffset;
                pModelParams[i].textureOffset = modelCreateParam.textureOffset;
                pModelParams[i].animationClipOffset = modelCreateParam.animationClipOffset;
                pModelParams[i].boneCount = modelCreateParam.boneCount;
            }
            //TextureArray
            for(int i = 0; i < _textureParams.Length; i++)
            {
                var textureParam = _textureParams[i];
                _material.SetTexture(textureParam.spTextureId, textureParam.textureArray);
            }
            // Animations
            if(_gbBoneMatrices != null)
                _gbBoneMatrices.Dispose();
            var lastAnimationClipCreateParam = lastModelCreateParam.animationClipCreateParams[lastModelCreateParam.animationClipCreateParams.Length - 1];
            var boneMatrixCount = lastAnimationClipCreateParam.boneMatrixOffset + lastAnimationClipCreateParam.boneMatrices.Length;
            _gbBoneMatrices = new GraphicsBuffer(GraphicsBuffer.Target.Vertex | GraphicsBuffer.Target.Raw, (int)boneMatrixCount, sizeof(Matrix4x4));
            for(int i = 0; i < _modelCreateParams.Count; i++)
            {
                var modelCreateParam = _modelCreateParams[i];
                for(int j = 0; j < modelCreateParam.animationClipCreateParams.Length; j++)
                {
                    var animParam = modelCreateParam.animationClipCreateParams[j];
                    _gbBoneMatrices.SetData(animParam.boneMatrices, 0, animParam.boneMatrixOffset, animParam.boneMatrices.Length);
                }
            }
            _material.SetBuffer(_spBoneMatricesId, _gbBoneMatrices);

            var animCliipParamCount = lastModelCreateParam.animationClipOffset + lastModelCreateParam.animationClipCreateParams.Length;
            if(_animationClipParams.IsCreated == true)
                _animationClipParams.Dispose();
            _animationClipParams = new NativeArray<AnimationClipParam>(animCliipParamCount, Allocator.Persistent);
            var pAnimationClipParams = (AnimationClipParam*)NativeArrayUnsafeUtility.GetUnsafePtr(_animationClipParams);
            var index = 0;
            for(int i = 0; i < _modelCreateParams.Count; i++)
            {
                var modelCreateParam = _modelCreateParams[i];
                for(int j = 0; j < modelCreateParam.animationClipCreateParams.Length; j++)
                {
                    var animParam = modelCreateParam.animationClipCreateParams[j];
                    pAnimationClipParams[index].boneMatrixOffset = animParam.boneMatrixOffset;
                    pAnimationClipParams[index].frameCount = animParam.frameCount;
                    pAnimationClipParams[index].nameHash = animParam.nameHash;
                    index++;
                }
            }
        }
        if(_unitDirty == true)
        {
            _unitDirty = false;
            //Indices
            if(_gbIndices != null)
                _gbIndices.Dispose();
            var indexCount = 0;
            for(int i = 0; i < unitCount; i++)
            {
                var modelCreateParam = _modelCreateParams[_units[i].modelId];
                indexCount += modelCreateParam.gbIndices.count;
            }
            _gbIndices = new GraphicsBuffer(GraphicsBuffer.Target.Index | GraphicsBuffer.Target.Raw, indexCount, sizeof(UInt32));
            var indexOffset = 0;
            var kernelIndex = _computeShader.FindKernel("GenerateIndices");
            uint numThreadX, numThreadY, numThreadZ;
            _computeShader.GetKernelThreadGroupSizes(kernelIndex, out numThreadX, out numThreadY, out numThreadZ);
            _computeShader.SetBuffer(kernelIndex, _spDestIndices, _gbIndices);
            for(int i = 0; i < unitCount; i++)
            {
                var modelCreateParam = _modelCreateParams[_units[i].modelId];
                _computeShader.SetInt(_spDestOffsetIndex, indexOffset * sizeof(UInt32));
                _computeShader.SetInt(_spSrcIndexCount, modelCreateParam.gbIndices.count);
                _computeShader.SetInt(_spSrcUnitIndex, i);
                _computeShader.SetInt(_spSrcVertexOffset, modelCreateParam.vertexOffset);
                _computeShader.SetBuffer(kernelIndex, _spSrcIndices, modelCreateParam.gbIndices);
                var groupLoopCount = (modelCreateParam.gbIndices.count + (int)(numThreadX - 1)) / (int)numThreadX;
                _computeShader.Dispatch(kernelIndex, groupLoopCount, 1, 1);
                indexOffset += modelCreateParam.gbIndices.count;
            }
            //IndirecctArgument
            var argInstanceNum = 1;
            var argIndexOffset = 0;
            var argVertexOffset = 0;
            var argInstanceOffset = 0;  //zero only?
            var args = new int[]{_gbIndices.count, argInstanceNum, argIndexOffset, argVertexOffset, argInstanceOffset};
            var stride = Buffer.ByteLength(args);
            if(_gbArgs != null)
                _gbArgs.Dispose();
            _gbArgs = new GraphicsBuffer(GraphicsBuffer.Target.IndirectArguments, 1, stride);
            _gbArgs.SetData(args);
            //UnitDesc
            if(_gbUnitDescs != null)
                _gbUnitDescs.Dispose();
            _gbUnitDescs = new GraphicsBuffer(GraphicsBuffer.Target.Vertex | GraphicsBuffer.Target.Raw, unitCount, sizeof(UnitDesc));
            using(var unitDescs = new NativeArray<UnitDesc>(unitCount, Allocator.Temp))
            {
                var pUnitDesc = (UnitDesc*)NativeArrayUnsafeUtility.GetUnsafePtr(unitDescs);
                for(int i = 0; i < unitCount; i++)
                {
                    var unit = _units[i];
                    pUnitDesc[i].modelId = unit.modelId;
                }
                _gbUnitDescs.SetData(unitDescs);
            }
            _material.SetBuffer(_spUnitDescsId, _gbUnitDescs);
            //UnitUpdateDesc
            if(_gbUnitUpdateDescs != null)
                _gbUnitUpdateDescs.Dispose();
            _gbUnitUpdateDescs = new GraphicsBuffer(GraphicsBuffer.Target.Vertex | GraphicsBuffer.Target.Raw, unitCount, sizeof(UnitUpdateDesc));
            if(_unitUpdateDescs.IsCreated == true)
                _unitUpdateDescs.Dispose();
            _unitUpdateDescs = new NativeArray<UnitUpdateDesc>(unitCount, Allocator.Persistent);
            _material.SetBuffer(_spUnitUpdateDescsId, _gbUnitUpdateDescs);
        }

        var job = new JobUnits()
        {
            units = _units.AsDeferredJobArray(),
            unitUpdateDescs = _unitUpdateDescs,
            modelParams = _modelParams,
            animationClipParams = _animationClipParams,
            deltaTime = Time.deltaTime,
        };
        _jhUnits = job.Schedule(_units.Length, 1);
        JobHandle.ScheduleBatchedJobs();
    }
}

TestDrawProcedual.compute

#pragma kernel GenerateVertices

ByteAddressBuffer SrcVertices;
uint SrcVertexCount;
int SrcStride;
RWByteAddressBuffer DestVertices;
int DestStride;
int DestOffsetVertex;
int DestOffsetInStride;

[numthreads(1,32,1)]
void GenerateVertices(uint2 id : SV_DispatchThreadID)
{
    if (id.y >= SrcVertexCount)
        return;
    int srcOffsetVertex = id.y * SrcStride;
    int destOffsetVertex = DestOffsetVertex + id.y * DestStride;

    int offsetInStride = id.x * 4;
    int destOffsetInStride = DestOffsetInStride + offsetInStride;
    int value = SrcVertices.Load(srcOffsetVertex + offsetInStride);
    DestVertices.Store(destOffsetVertex + destOffsetInStride, value);
}

#pragma kernel GenerateIndices

ByteAddressBuffer SrcIndices;
uint SrcIndexCount;
uint SrcUnitIndex;
uint SrcVertexOffset;
RWByteAddressBuffer DestIndices;
int DestOffsetIndex;

[numthreads(128, 1, 1)]
void GenerateIndices(uint id : SV_DispatchThreadID)
{
    if (id.x >= SrcIndexCount)
        return;
    int destOffsetIndex = DestOffsetIndex + id.x * 4;
    int srcOffsetIndex = id.x * 4;
    uint value = SrcIndices.Load(srcOffsetIndex);
    value = (SrcUnitIndex << 22) | ((SrcVertexOffset + value) & 0x3fffff);
    DestIndices.Store(destOffsetIndex, value);
}

TestDrawProcedual.shader

Shader "Test/TestDrawProcedual"
{
    Properties
    {
        _MainTex("Tex", 2DArray) = "" {}
    }

    CGINCLUDE
    #include "UnityCG.cginc"
    #include "AutoLight.cginc"
    #pragma require 2darray

    struct appdata
    {
        uint vertexId : SV_VertexID;
        uint instanceId : SV_InstanceID;
    };

    struct VertexAndNormal
    {
        float4 vertex;
        float3 normal;
    };

    struct VertexStream
    {
        float3 position;
        float3 normal;
        float4 tangent;
        float2 uv;
        float4 blendWeight;
        int4 blendIndices;
    };
    StructuredBuffer<VertexStream> _Vertices;
    StructuredBuffer<float4x4> _BoneMatrices;
    struct UnitDesc
    {
        int modelId;
    };
    StructuredBuffer<UnitDesc> _UnitDescs;
    struct UnitUpdateDesc
    {
        float4x4 worldMatrix;
        int boneMatrixId;
    };
    StructuredBuffer<UnitUpdateDesc> _UnitUpdateDescs;
    UNITY_DECLARE_TEX2DARRAY(_MainTex);

    void GetBoneMatrices(VertexStream vertex, UnitUpdateDesc unitUdapteDesc, out float4x4 boneMatrix0, out float4x4 boneMatrix1, out float4x4 boneMatrix2, out float4x4 boneMatrix3)
    {
        int animMatrixId = unitUdapteDesc.boneMatrixId;
        boneMatrix0 = _BoneMatrices[animMatrixId + vertex.blendIndices.x];
        boneMatrix1 = _BoneMatrices[animMatrixId + vertex.blendIndices.y];
        boneMatrix2 = _BoneMatrices[animMatrixId + vertex.blendIndices.z];
        boneMatrix3 = _BoneMatrices[animMatrixId + vertex.blendIndices.w];
    }

    void ComputeBone(VertexStream vertex, UnitUpdateDesc unitUdapteDesc, out float4 pos, out float3 normal)
    {
        float4x4 boneMatrix0, boneMatrix1, boneMatrix2, boneMatrix3;
        GetBoneMatrices(vertex, unitUdapteDesc, boneMatrix0, boneMatrix1, boneMatrix2, boneMatrix3);

        pos = float4(vertex.position, 1.0);
        pos =   mul(boneMatrix0, pos) * vertex.blendWeight.x +
                    mul(boneMatrix1, pos) * vertex.blendWeight.y +
                    mul(boneMatrix2, pos) * vertex.blendWeight.z +
                    mul(boneMatrix3, pos) * vertex.blendWeight.w;

        normal =    mul(boneMatrix0, vertex.normal) * vertex.blendWeight.x +
                        mul(boneMatrix1, vertex.normal) * vertex.blendWeight.y +
                        mul(boneMatrix2, vertex.normal) * vertex.blendWeight.z +
                        mul(boneMatrix3, vertex.normal) * vertex.blendWeight.w;
    }

    void ComputeBone(VertexStream vertex, UnitUpdateDesc unitUdapteDesc, out float4 pos)
    {
        float4x4 boneMatrix0, boneMatrix1, boneMatrix2, boneMatrix3;
        GetBoneMatrices(vertex, unitUdapteDesc, boneMatrix0, boneMatrix1, boneMatrix2, boneMatrix3);

        pos = float4(vertex.position, 1.0);
        pos = mul(boneMatrix0, pos) * vertex.blendWeight.x +
            mul(boneMatrix1, pos) * vertex.blendWeight.y +
            mul(boneMatrix2, pos) * vertex.blendWeight.z +
            mul(boneMatrix3, pos) * vertex.blendWeight.w;
    }

    ENDCG

    SubShader
    {
        Tags{ "LightMode" = "ForwardBase" "RenderType" = "Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma target 5.0
            #pragma multi_compile_fwdbase
            #pragma vertex vert
            #pragma fragment frag

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 uv : TEXCOORD0;
                SHADOW_COORDS(1)
            };

            v2f vert(appdata i)
            {
                v2f o;
                uint descId = (i.vertexId >> (22)) & 0x3ff;
                uint vertexId = i.vertexId & 0x3fffff;

                UnitDesc unitDesc = _UnitDescs[descId];
                VertexStream vertex = _Vertices[vertexId];
                UnitUpdateDesc unitUdapteDesc = _UnitUpdateDescs[descId];

                VertexAndNormal v;
                ComputeBone(vertex, unitUdapteDesc, v.vertex, v.normal);

                unity_ObjectToWorld = unitUdapteDesc.worldMatrix;
                o.pos = mul(UNITY_MATRIX_VP, mul(unity_ObjectToWorld, v.vertex));
                o.uv = float3(vertex.uv, unitDesc.modelId);
                TRANSFER_SHADOW(o)
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                fixed4 col = UNITY_SAMPLE_TEX2DARRAY(_MainTex, i.uv);
                fixed shadow = max(SHADOW_ATTENUATION(i), 0.5);
                return col * shadow;
            }
            ENDCG
        }

        Pass
        {
            Name "ShadowCaster"
            Tags{"LightMode" = "ShadowCaster"}
            Zwrite On
            ZTest LEqual

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_shadowcaster
            #pragma target 5.0

            struct v2f
            {
                float3 uv : TEXCOORD0;
                V2F_SHADOW_CASTER;
            };

            inline float3 CustomObjectToWorldDir(in float3 dir, in float3x3 worldMatrix)
            {
                return normalize(mul(worldMatrix, dir));
            }
            inline float4 CustomClipSpaceShadowCasterPos(in float4 vertex, in float3 normal, in float4x4 worldMatrix)
            {
                float4 wPos = mul(worldMatrix, vertex);
                if (unity_LightShadowBias.z != 0.0)
                {
                    float3 wNormal = CustomObjectToWorldDir(normal, (float3x3)worldMatrix);
                    float3 wLight = normalize(CustomObjectToWorldDir(wPos.xyz, (float3x3)worldMatrix));
                    float shadowCos = dot(wNormal, wLight);
                    float shadowSine = sqrt(1 - shadowCos * shadowCos);
                    float normalBias = unity_LightShadowBias.z * shadowSine;
                    wPos.xyz -= wNormal * normalBias;
                }
                return mul(UNITY_MATRIX_VP, wPos);
            }

            v2f vert(appdata i)
            {
                v2f o;
                uint descId = (i.vertexId >> (22)) & 0x3ff;
                uint vertexId = i.vertexId & 0x3fffff;

                UnitDesc unitDesc = _UnitDescs[descId];
                VertexStream vertex = _Vertices[vertexId];
                UnitUpdateDesc unitUdapteDesc = _UnitUpdateDescs[descId];

                VertexAndNormal v;
                ComputeBone(vertex, unitUdapteDesc, v.vertex, v.normal);

                unity_ObjectToWorld = unitUdapteDesc.worldMatrix;
                o.uv = float3(vertex.uv, unitDesc.modelId);

                TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                fixed4 col = UNITY_SAMPLE_TEX2DARRAY(_MainTex, i.uv);
                SHADOW_CASTER_FRAGMENT(i)
            }
            ENDCG
        }
    }
}