Quick answer: Compute boneMatrices[i] = bones[i].localToWorldMatrix * mesh.bindposes[i]. Pass the result to the job. Schedule from LateUpdate after the SkinnedMeshRenderer’s own update. Vertex transform is then in world space.

You write a custom skinning job to apply effects on top of normal animation. Output is a tangled mesh because bone-local positions land in the wrong space.

The Symptom

Custom skinned mesh deformer renders verts in wrong positions. Bones look right; mesh is shifted/rotated wildly.

The Fix

var bindPoses = mesh.bindposes;
var boneMatrices = new NativeArray<float4x4>(bones.Length, Allocator.TempJob);

for (int i = 0; i < bones.Length; i++)
    boneMatrices[i] = bones[i].localToWorldMatrix * bindPoses[i];

var job = new SkinJob {
    boneMatrices = boneMatrices,
    boneIndices  = _boneIndicesNative,
    boneWeights  = _boneWeightsNative,
    inVerts      = _inVerts,
    outVerts     = _outVerts
};
var handle = job.Schedule(_inVerts.Length, 64);
handle.Complete();
boneMatrices.Dispose();

The bindpose multiply is the key. Verts get transformed: bind-pose-inverse * world-bone = world position.

Inside the Job

[BurstCompile]
struct SkinJob : IJobParallelFor
{
    [ReadOnly] public NativeArray<float4x4> boneMatrices;
    [ReadOnly] public NativeArray<int4>     boneIndices;
    [ReadOnly] public NativeArray<float4>  boneWeights;
    [ReadOnly] public NativeArray<float3>  inVerts;
    public           NativeArray<float3>  outVerts;

    public void Execute(int i)
    {
        var p = new float4(inVerts[i], 1);
        var idx = boneIndices[i];
        var w   = boneWeights[i];
        var result = math.mul(boneMatrices[idx.x], p) * w.x
                   + math.mul(boneMatrices[idx.y], p) * w.y
                   + math.mul(boneMatrices[idx.z], p) * w.z
                   + math.mul(boneMatrices[idx.w], p) * w.w;
        outVerts[i] = result.xyz;
    }
}

Timing

SkinnedMeshRenderer updates in its own LateUpdate. Schedule after, or via PlayerLoop hook in PostLateUpdate. From script, LateUpdate works for most cases.

Verifying

Compare your output to a stock SkinnedMeshRenderer. Should match exactly when no extra deformation is applied. If skewed, the bindpose multiply is missing or order is reversed.

“localToWorld * bindpose. World-space verts. Job in LateUpdate.”

Related Issues

For SMR bounds, see SMR bounds. For 2D Animation rig, see 2D rig.

Bind pose multiply. Right space. Mesh deforms.