Obi Official Forum

Full Version: ObiSkinnedCloth.SetTargetSkin Jobification
You're currently viewing a stripped down version of our content. View the full version with proper formatting.
posting this here so it can be verified and hopefully merged into the project (unless supersed already by better code):
Modifications in ObiSkinnedCloth.cs:
Code:
//Top of file:
using Unity.Jobs;
using Unity.Collections;
using Unity.Burst;
using Unity.Mathematics;

...

//replaces existing sortedPoints/Normals
private NativeArray<Vector4> sortedPoints;
private NativeArray<Vector4> sortedNormals;
private NativeArray<int> actorIndices;

...

//Changing LoadBlueprint to init native arrays
public override void LoadBlueprint(ObiSolver solver)
{
    base.LoadBlueprint(solver);
    SetupRuntimeConstraints();

    var skinConstraints = GetConstraintsByType(Oni.ConstraintType.Skin) as ObiConstraints<ObiSkinConstraintsBatch>;
    if (skinConstraints != null && skinConstraints.GetBatchCount() > 0)
    {
        var batch = skinConstraints.batches[0] as ObiSkinConstraintsBatch;
        if(sortedPoints.IsCreated)
        {
            sortedPoints.Dispose();
        }
        sortedPoints = new NativeArray<Vector4>(batch.constraintCount, Allocator.Persistent);

        if(sortedNormals.IsCreated)
        {
            sortedNormals.Dispose();
        }
        sortedNormals = new NativeArray<Vector4>(batch.constraintCount, Allocator.Persistent);

        if(actorIndices.IsCreated)
        {
            actorIndices.Dispose();
        }
        actorIndices = new NativeArray<int>(batch.constraintCount, Allocator.Persistent);
    }
}

...

//Adding on destroy to cleanup native arrays
private void OnDestroy()
{
    if(sortedPoints.IsCreated)
    {
        sortedPoints.Dispose();
    }

    if(sortedNormals.IsCreated)
    {
        sortedNormals.Dispose();
    }

    if(actorIndices.IsCreated)
    {
        actorIndices.Dispose();
    }
}


private void SetTargetSkin()
{
    using (m_SetTargetSkinPerfMarker.Auto())
    {

        var skinConstraints = GetConstraintsByType(Oni.ConstraintType.Skin) as ObiConstraints<ObiSkinConstraintsBatch>;
        var batch = skinConstraints.batches[0] as ObiSkinConstraintsBatch;

        using (m_SortSkinInputsPerfMarker.Auto())
        {
            int pointCount = bakedVertices.Count;
            for (int i = 0; i < pointCount; ++i)
            {
                int welded = m_SkinnedClothBlueprint.topology.rawToWelded[i];
                sortedPoints[welded] = bakedVertices[i];
                sortedNormals[welded] = bakedNormals[i];
            }
        }

        using (m_SetSkinInputsPerfMarker.Auto())
        {
            for(int i = 0; i < batch.activeConstraintCount; ++i)
            {
                int solverIndex = batch.particleIndices[i];
                actorIndices[i] = solver.particleToActor[solverIndex].indexInActor;
            }

            setSkinInputsJob.approxValue = Mathf.Epsilon * 8f;
            setSkinInputsJob.skinToSolver = actorLocalToSolverMatrix;

            setSkinInputsJob.sortedPoints = sortedPoints;
            setSkinInputsJob.sortedNormals = sortedNormals;

            setSkinInputsJob.particleIndices = batch.particleIndices.AsNativeArray<int>();
            setSkinInputsJob.actorIndices = actorIndices;

            setSkinInputsJob.skinCompliance = batch.skinCompliance.AsNativeArray<float>();
            setSkinInputsJob.skinRadiiBackstop = batch.skinRadiiBackstop.AsNativeArray<float>();

            setSkinInputsJob.skinPoints = batch.skinPoints.AsNativeArray<Vector4>();
            setSkinInputsJob.skinNormals = batch.skinNormals.AsNativeArray<Vector4>();

            setSkinInputsJob.invMasses = solver.invMasses.AsNativeArray<float>();
            setSkinInputsJob.positions = solver.positions.AsNativeArray<Vector4>();

            setSkinInputsJob.Run();
        }
    }
}

SetSkinInputsJob setSkinInputsJob = new SetSkinInputsJob();

[BurstCompile(FloatMode = FloatMode.Deterministic)]
struct SetSkinInputsJob : IJob
{
    public float approxValue;
    public Matrix4x4 skinToSolver;

    [ReadOnly]
    public NativeArray<Vector4> sortedPoints;
    [ReadOnly]
    public NativeArray<Vector4> sortedNormals;

    [ReadOnly]
    public NativeArray<int> particleIndices;
    [ReadOnly]
    public NativeArray<int> actorIndices;

    [ReadOnly]
    public NativeArray<float> skinRadiiBackstop;
    [ReadOnly]
    public NativeArray<float> skinCompliance;

    //[WriteOnly]
    public NativeArray<Vector4> skinPoints;
    [WriteOnly]
    public NativeArray<Vector4> skinNormals;

    [WriteOnly]
    public NativeArray<float> invMasses;
    [WriteOnly]
    public NativeArray<Vector4> positions;

    public void Execute()
    {
        for(int i = 0; i < particleIndices.Length; ++i)
        {
            int solverIndex = particleIndices[i];
            int actorIndex = actorIndices[i];

            skinPoints[i] = skinToSolver.MultiplyPoint3x4(sortedPoints[actorIndex]);
            skinNormals[i] = skinToSolver.MultiplyVector(sortedNormals[actorIndex]);

            // Rigidly transform particles with zero skin radius and zero compliance:                   
            if(math.abs(skinRadiiBackstop[i * 3]) <= approxValue && math.abs(skinCompliance[i]) <= approxValue)
            {
                invMasses[solverIndex] = 0;
                positions[solverIndex] = skinPoints[i];
            }
        }
    }
}
I assume there's some potential optimisation by switching to float4x4s and float4s across the board, but I've had no luck implementing that, and the job is already very fast
You could also reduce some of the job data assignment calls probs.

For me it reduced the task from ~0.03-0.04ms per SkinnedCloth with about 50-80 vertices down to ~0.02-0.025ms
Hi,

This is fine, but only works if you have Burst, Jobs, and other dependencies installed. Code outside of a specific backend should compile and run with no external dependencies of any kind (as you can't assume which backend the engine is using).

I've been working on moving the entire render/mesh updating pipeline to backends. In most cases this allows to merge data from different actors into the same array, then schedule a single job per solver and work in parallel over multiple actors at once. This is similar to what constraint batches already do, results in better performance in all cases and works for any backend. This is a quite large change though and currently R&D, so it won't be available until Obi 7.0.
Didn't know this was burst-specific, but we're limited to that because we need the multi-platform support.

I'll be looking towards 7.0 and keep this in for now then! Sonrisa
(07-06-2021, 08:13 AM)ThoughtMango Wrote: [ -> ]Didn't know this was burst-specific, but we're limited to that because we need the multi-platform support.

I'll be looking towards 7.0 and keep this in for now then! Sonrisa

More than Burst specific, it's Jobs specific. However using jobs without Burst is kinda unusual, as Burst is basically free performance Sonrisa
Thanks for the feedback!