Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Help  Get separate particles parts
#7
You will first need to modify the ObiFixedUpdater.cs component attached to your ObiSolver
so we can do ObiFixedUpdater.PostFixedUpdate += ComputeUnionFind;
You will also need to manage list sizes by subscribing to ObiSolver.OnEnsureParticleArrayCapacity

Code:
public System.Action PreFixedUpdate;
public System.Action PostFixedUpdate;

private void FixedUpdate()
{
    ObiProfiler.EnableProfiler();

    PrepareFrame();

    BeginStep(Time.fixedDeltaTime);
    PreFixedUpdate?.Invoke();           // nice option to have

    float substepDelta = Time.fixedDeltaTime / (float)substeps;

    // Divide the step into multiple smaller substeps:
    for (int i = 0; i < substeps; ++i)
        Substep(Time.fixedDeltaTime, substepDelta, substeps-i);

    EndStep(substepDelta);
    PostFixedUpdate?.Invoke();        // do Union Find after particle simulation

    ObiProfiler.DisableProfiler();

    accumulatedTime -= Time.fixedDeltaTime;
}

Here is the rest of the code for managing the UnionFind.

It's quite error-prone, so includes lots of debugging, but it ought to get you most of the way towards getting the particle groups you were looking for. Just create one "group" (i call it a Gamebody) for each in rootSolverIndices, and iterate the rest of the particles to see which all reference that root by using m_perParticle_RootSolverIndex.

Code:
ObiNativeIntList m_perParticle_RootSolverIndex;    // count should always match ObiSolver.positions
ObiNativeBoolList m_solverIndexActive;      // see ObiSolver.OnSetActiveParticles and ObiSolver.activeIndices

void ComputeUnionFind()
{
    ResetUnionFindElements(m_perParticle_RootSolverIndex);
    Merge_NonFluids(m_perParticle_RootSolverIndex);
    Merge_Fluids(m_perParticle_RootSolverIndex);

    UnionFind_GetRoots(m_perParticle_RootSolverIndex, out List<int> rootSolverIndices);
    Unionfind_SanityCheckRoots(rootSolverIndices);

    // ... re-iterate all active solver indices to handle the sets of particles
}
void SetActiveParticles(NativeArray<int> actives)
{
    for (int i = 0; i < m_solverIndexActive.count; i++)
        m_solverIndexActive[i] = false;
    foreach (int active in actives)
        m_solverIndexActive[active] = true;
}

#region ObiUnionfind

// Set all solver indices to Self (index) or -1, inactive
// after UnionFind, those that still point to themselves are the roots of their fluid bodies
private void ResetUnionFindElements(ObiNativeIntList solverRoots)
{
    PrepUnionFind_RootIndices prepJob = new PrepUnionFind_RootIndices() {
        solverRoots = solverRoots.AsNativeArray<int>(),
        solverIndexActive = m_solverIndexActive.AsNativeArray<bool>(),
    };
    JobHandle clearHandle = prepJob.Schedule(solver.positions.count, 16);        // Iterate ALL particles, not just active/instanced. do not use allocParticles
    clearHandle.Complete();
}
[BurstCompile]
public struct PrepUnionFind_RootIndices : IJobParallelFor
{
    [WriteOnly] public NativeArray<int> solverRoots;
    [ReadOnly] public NativeArray<bool> solverIndexActive;

    public void Execute(int solverIndex)
    {
        int elementRoot = UnionFindGamebodies.DisabledElement;  // Inactive particles will always point to -1.
        if (solverIndexActive[solverIndex])
            elementRoot = UnionFindGamebodies.SelfElement(solverIndex);  // Initially, all Active Indices are roots - point to self

        solverRoots[solverIndex] = elementRoot;
    }
}

// For Rope and Cloth, point the entire actor towards the lowest solver index
private void Merge_NonFluids(ObiNativeIntList rootSolverIndices)
{
    Debug.Assert(rootSolverIndices.count == solver.positions.count, $"Length of UnionFind roots must match solver Posiitions  couns\n{rootSolverIndices.count} != {solver.positions.count}");

    foreach (var actor in solver.actors)
    {
        if (actor is ObiEmitter) // Ignore Fluids
            continue;

        // Merge all Solver Indices
        int lowestSolverIndex = -1;   // Just choose the FIRST index to call Merge() all others. Internally, UnionFind will point everyone to the Lowest.
        for (int actorIndex = 0; actorIndex < actor.activeParticleCount; actorIndex++)  // Actives Only
        {
            int solverIndex = actor.solverIndices[actorIndex];
            if (lowestSolverIndex < 0)
            {
                lowestSolverIndex = solverIndex;
                continue;
            }

            TryMergeRoots(rootSolverIndices, lowestSolverIndex, solverIndex);
        }
    }
}
// Use interaction pairs to detect contiguous Fluid Bodies of Obi particles
private void Merge_Fluids(ObiNativeIntList rootSolverIndices)
{
    // sanity checks
    Debug.Assert(solver.implementation != null, $"{nameof(solver.implementation)} is null, is ObiSolver not yet inited?");
    Debug.Assert(rootSolverIndices.count == solver.positions.count, $"Length of UnionFind roots must match solver Posiitions  couns\n{rootSolverIndices.count} != {solver.positions.count}");

    // Should probably reference your particle size. You are also free to try removing all distance checking, because Obi already said these two particles are "interacting"
    float distanceThreshold = Mathf.Pow(solverHandler.m_gamebodyMergeMaxDist, 2);
    NativeArray<FluidInteraction> fluidSimplexInteractions = (solver.implementation as BurstSolverImpl).fluidInteractions;
    foreach (var interaction in fluidSimplexInteractions)
    {
        // Fluid Interactions contain Solver Indices
        if (interaction.particleA >= solver.positions.count || interaction.particleB >= solver.positions.count)
        {
            Debug.LogError($"Solver Particle(s) interaction invalid: {interaction.particleA} {interaction.particleB} for solver positions count {solver.positions.count}");
            continue;
        }

        // ensure that particles are active in actors
        //     An additional ObiNativeBoolList m_solverIndexActive would allow
        //     bool isactiveA = m_solverIndexActive[interaction.particleA]
        var actorinfoA = solver.particleToActor[interaction.particleA];
        var actorinfoB = solver.particleToActor[interaction.particleB];
        bool isactiveA = actorinfoA.indexInActor < actorinfoA.actor.activeParticleCount;
        bool isactiveB = actorinfoB.indexInActor < actorinfoB.actor.activeParticleCount;
        if (isactiveA == false || isactiveB == false)
        {
            //Debug.LogWarning($"Interaction ignored, one or both is inactive");
            continue;
        }

        // quick distance check with sqrMagnitude
        Vector3 posA = solver.positions[interaction.particleA];
        Vector3 posB = solver.positions[interaction.particleB];
        float sqrDist = (posA - posB).sqrMagnitude;
        if (sqrDist > distanceThreshold)
            continue;

        // We are now positive that particles A and B have interacted, and can thus be assumed to be in the same contiguous fluid body
        // Therefore, re-direct the upper index to the lower index
        // So that the lowest index of any fluidbody is the root index of all particles in that body
        TryMergeRoots(rootSolverIndices, interaction.particleA, interaction.particleB);
    }
}
// After merging, we can find the roots
public void UnionFind_GetRoots(ObiNativeIntList solverRoots, out List<int> rootSolverParticles)
{
    List<string> errors = new();
    List<int> errSolverIndices = new();
    var rootSolverParticlesSet_SolverIndex = new HashSet<int>();
    foreach (var obiActor in solver.actors)
    {
        int actorIndex = 0;

        // For 0..activeCount, (inclusive, exclusive) 
        for (; actorIndex < obiActor.activeParticleCount; actorIndex++)
        {
            int solverIndex = obiActor.solverIndices[actorIndex];
            int solverRoot = solverRoots[solverIndex];

            if (solverRoot == DisabledElement)      // save errors for later
            {
                int iOfActor = solver.actors.IndexOf(obiActor);
                var err = $"Actor{iOfActor} p{actorIndex}<active{obiActor.activeParticleCount} BUT root elem at solver{solverIndex} is DISABLED({solverRoot})!!";
                errors.Add(err); errSolverIndices.Add(solverIndex);
            }

            if (solverRoots[solverIndex] == SelfElement(solverIndex))
                rootSolverParticlesSet_SolverIndex.Add(solverIndex);
        }

        // Continue onwards and double-check the inactive particles
        // For activeCount..allocCount (inclusive, exclusive)
        for (; actorIndex < obiActor.particleCount; actorIndex++)
        {
            int solverIndex = obiActor.solverIndices[actorIndex];
            int solverRoot = solverRoots[solverIndex];

            if (solverRoot != DisabledElement)       // save errors for later
            {
                int iOfActor = solver.actors.IndexOf(obiActor);
                var err = $"Actor{iOfActor} p{actorIndex}>=active{obiActor.activeParticleCount} BUT root elem at solver{solverIndex} is NOT disabled({solverRoot})!!";
                errors.Add(err); errSolverIndices.Add(solverIndex);
            }
        }
    }
    // Done!!
    rootSolverParticles = rootSolverParticlesSet_SolverIndex.ToList();

    // print any errors
    if (errors.Count > 0 && solver.actors.Count > 0)
    {
        ObiActor actor = solver.actors[0];
        List<(int, int)> errSolverAndActor = new();
        foreach (int errSolver in errSolverIndices)
        {
            var iInActor = m_indexSolverToActors[errSolver].iInActor;
            errSolverAndActor.Add((errSolver, iInActor.Int));
        }

        // Print out the tuples for debugging
        string rootsString = string.Join(", ", solverRoots.Select(r => r));
        string actorSolverIndStr = string.Join(", ", actor.solverIndices.Select(s => s));
        string unionString = string.Join(", ", errSolverAndActor.Select(t => $"(s{t.Item1}, a{t.Item2})"));
        string unionErr = string.Join("\n", errors.Select(e => e));
        Debug.LogError($"Bad roots! {rootsString}\n" +
            $"actorSolverIndices: {actorSolverIndStr} numActiveAct0: {actor.activeParticleCount}\n" +
            $"errSolverAndActor: {unionString}\n" +
            $"{unionErr}");
    } // erorr checking
}

// Make sure the mess of indices was handled properly
void Unionfind_SanityCheckRoots(List<int> rootSolverIndices)
{
    if (rootSolverIndices.Count == solver.positions.count)
        Debug.LogWarning($"UnionFind: It appears there is 1 Root particle assigned for every particle in the Solver. {rootSolverIndices.Count}=={solver.positions.count}. This is likely an error. Check particle interaction queue's handling, or UnionFind.");
    // Each root particle must be active
    foreach (int solverIndex in rootSolverIndices)
    {
        if (solverIndex >= solver.positions.count) Debug.LogError($"index {solverIndex} not in count {solver.positions.count}");

        var actorInfo = solver.m_ParticleToActor[solverIndex];
        if (actorInfo.indexInActor < 0 || actorInfo.indexInActor >= actorInfo.actor.activeParticleCount)
            Debug.LogWarning($"Inactive solver particle:{solverIndex} in actor {solver.actors.IndexOf(actorInfo.actor)} iInActor:{actorInfo.indexInActor}");
    }
    // If there are any particles, surely we found some roots!
    if (solver.activeParticles.count > 0 && rootSolverIndices.Count == 0)
    {
        int solverActive = solver.activeParticles.count;
        bool dirtyActive = solver.dirtyActiveParticles;
        List<int> actorActives = new List<int>(); // num active in actor
        List<int> solverActives = new List<int>(); // all solver particles, active in actor?
        foreach (var actor in solver.actors)
        {
            actorActives.Add(actor.activeParticleCount);
            for (int i = 0; i < actor.particleCount; i++)
                solverActives.Add(i < actor.activeParticleCount ? 1 : 0);
        }
        string actorsString = string.Join(",", actorActives);
        string solverString = string.Join(",", solverActives);
        List<int> root = m_perParticle_RootSolverIndex.ToList<int>(); // root particles
        string[] rootStrings = new string[root.Count];
        for (int i = 0; i < root.Count; i++) rootStrings[i] = (root[i] == SelfElement(i)) ? $"{root[i].ToString()}(S)" : root[i].ToString();
        string rootsString = string.Join(",", rootStrings);                                                                                // DEBUGS
        Debug.LogError($"{solver.activeParticles.count} solver particles active, yet 0 union roots! numActiveInActors:[{actorsString}]!\n" + "" +
            $"{"solver roots":d15}: {rootsString}\n" +
            $"{"activeInActor":d15}: {solverString}\n" +
            $"{"NOTE:d15"}: there is a reference to an inactive Solver Index, yes?");
        return;
    }
}

#endregion ObiUnionfind








#region Union-Find Helpers

public static readonly int DisabledElement = -1;
public static int SelfElement(int solverIndex) // Root elements point to self
{
    return solverIndex;
}

private bool TryMergeRoots(ObiNativeIntList particleRoots, int solverIndexA, int solverIndexB)
{
    int rootA = FindRoot(particleRoots, solverIndexA);
    int rootB = FindRoot(particleRoots, solverIndexB);
    if (rootA == rootB)  // Already merged? Great.
        return false;

    MergeRoots(particleRoots, rootA, rootB);
    return true;
}
private int FindRoot(ObiNativeIntList solverRoots, int solverIndex)
{
    // Look for an element that points to itself
    int queryIndex = solverIndex;
    if (queryIndex >= solverRoots.count)
        Debug.LogError($"Query Index {queryIndex} too big for count {solverRoots.count}");

    while (solverRoots[queryIndex] != SelfElement(queryIndex))
    {
        if (solverRoots[queryIndex] == DisabledElement)  // Why does it point to disabled?
            break;
        queryIndex = solverRoots[queryIndex];    // Read
    }

    int rootIndex = queryIndex;
    PathCompress(solverRoots, solverIndex, rootIndex);   // Write
    return rootIndex;
}
void MergeRoots(ObiNativeIntList solverRoots, int rootA, int rootB)
{
    // For fluidbody association, Prefer to point to Smaller root index  roots[b] = a
    // For Single-Thread, this removes Randomness (Camera Jitter for multiple slimes)
    Debug.Assert(rootA != rootB, $"Why does {rootA}=={rootB}?");
    if (rootA > rootB)   // As preferred above, don't want solverRoots[rootB] = rootA; with large A because root should be lower
    {
        int tmp = rootA;
        rootA = rootB;
        rootB = tmp;
    }
    Debug.Assert(rootA < rootB);
    solverRoots[rootB] = rootA;
}

// While associating particle interaction pairs, avoid keeping multiple redirects
// For any particle whose root>...>root is i, point directly to i
void PathCompress(ObiNativeIntList perParticleRootIndex, int solverIndex, int rootIndex)
{
    while (solverIndex != rootIndex)
    {
        int parent = perParticleRootIndex[solverIndex];   // Read
        perParticleRootIndex[solverIndex] = rootIndex;   // Write
        solverIndex = parent;
    }
}

#endregion UnionFind Helpers

P.S. You are free to remove the burst code and native lists, and use a For Loop and Lists. "PrepUnionFind_RootIndices" is the only Burst Job, nothing else is parallelized. (I have no clue how to parallelize UnionFind because atomic locks (neded for multi-threaded read/write to random array positions) in Unity seem impossible. Future work.)
Reply


Messages In This Thread
Get separate particles parts - by Zavhoz - 14-01-2025, 01:39 PM
RE: Get separate particles parts - by josemendez - 14-01-2025, 01:46 PM
RE: Get separate particles parts - by Zavhoz - 15-01-2025, 05:22 AM
RE: Get separate particles parts - by josemendez - 15-01-2025, 09:48 AM
RE: Get separate particles parts - by Zavhoz - 15-01-2025, 10:26 AM
RE: Get separate particles parts - by slimedev - Yesterday, 10:05 AM
RE: Get separate particles parts - by slimedev - Yesterday, 06:51 PM