Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Help  Rope saving and restore by Json
#11
(25-06-2024, 09:58 AM)josemendez Wrote: You'll need to store blueprint control points as well anyway, so the start/end points of each rope will be stored in your JSON. At this point you can probably just have one scene in your game and store the entire level setup in JSON.


Sure! let me know if you need further help Sonrisa

Hi, I'm back, sorry to bother you again. After some attempts, I have made some significant progress, which is really very exciting.  Guiño
But now I have encountered some new problems. I have uploaded the latest results and problem pictures in the attachment.


Following your advice, I have now saved the length and position of the particles, as well as the ControlPoints in the blueprint, while inTangent and outTangent are Vector.zero, so I didn't save it in order to save space. When instantiating the rope, the originally stored controlPoints are restored through the blueprint's AddControlPoints() method, and after the blueprint is fully loaded, the stored positions are reset to the particles, which allows for the restoration of the shape of the rope for the vast majority of the time as it was before it was saved. So now the generated rope is really similar to what it was before it was saved. But there is still some loss of detail, which directly affects the difficulty of the game in the actual gameplay.


Here is the code to save controlPoints.
Code:
JSONObject GetRopeControlPoints()
    {
        var rope = gameObject.GetComponent<ObiRope>();
        var pointsArray = JSONObject.emptyArray;
        //var inTangentArray = JSONObject.emptyArray;
        //var outTangentArray = JSONObject.emptyArray;
        foreach (var obiWingedPoint in rope.ropeBlueprint.path.m_Points.data)
        {
            pointsArray.Add((JSONNode)obiWingedPoint.position);
            //inTangentArray.Add((JSONNode)obiWingedPoint.inTangent);
            //outTangentArray.Add((JSONNode)obiWingedPoint.outTangent);
        }
        var jsonObject = JSONObject.emptyObject;
        jsonObject.AddField("points", pointsArray);
        //jsonObject.AddField("inTangent", inTangentArray);
        //jsonObject.AddField("outTangent", outTangentArray);
        return jsonObject;
    }


Here is the code when instantiating the rope.
Code:
        ObiRopeBlueprint blueprint = ScriptableObject.CreateInstance<ObiRopeBlueprint>();
        blueprint.path.Clear();
        blueprint.resolution = 1;
       
        int filter = ObiUtils.MakeFilter(ObiUtils.CollideWithEverything, 0);
       
        int posIndex = 0;
        foreach (var obiWingedPoint in controlPoints)
        {
            blueprint.path.AddControlPoint(obiWingedPoint.position, obiWingedPoint.inTangent, obiWingedPoint.outTangent,
                Vector3.up, ropeMass, 0.1f, ropeThickness, filter, Color.yellow, $"pos_{posIndex}");
            posIndex++;
        }
     
        blueprint.path.FlushEvents();
        blueprint.GenerateImmediate();     
   
        ObiParticleAttachment attachment1 = rpObject.AddComponent<ObiParticleAttachment>();
        ObiParticleAttachment attachment2 = rpObject.AddComponent<ObiParticleAttachment>();
     
        rope.ropeBlueprint = blueprint;
       
        attachment1.target = transformA;
        attachment2.target = transformB;
        attachment1.particleGroup = blueprint.groups[0];
        attachment2.particleGroup = blueprint.groups.Last();

        // Parent the actor under a solver to start the simulation:
        rpObject.transform.SetParent(obiSolverObject.transform);
       
        return rpObject;


The images in the attachment should sufficiently illustrate the situation of the problem. Here, I will provide a brief description as well, just in case the images cannot be viewed:  After the restoration, the shape of the rope is really close to it was saved. However, some areas where the ropes are twisted with each other are lost, regardless of the complexity of the twists. As can be seen in image 6, even simple windings can sometimes be lost, even though they had clearly occurred by the first frame.
From image 5, it can be observed that the particles extracted from rope.solverIndices are not contiguous in position, which I believe to be normal according to the Obi documentation. However, I am not certain if this is the cause of the loss of detail.

可以看到从rope.solverIndices中取出的particle,在位置上并不是连续在一起的,根据obi的资料后,我觉得这应该是正常的。但是不确定是否这是导致细节丢失的原因。
The order of the particles in rope.elements is coherent. I have tried to save and restore these particles, but the problem persists.

In the level, there are pre-placed Obi solvers with various parameters and constraints that have been adjusted. The instantiated rope has been saved as a prefab, and the parameters have also been adjusted. Therefore, the parameters and constraints applied when restoring the rope using the data should be the same as before the data was saved. Thus, I did not store the constraints in the JSON, and no operations were performed during restoration.

Is there any step that I have missed?


Attached Files Thumbnail(s)
           
Reply
#12
Hi,

You're setting the blueprint's resolution to 100. It can't go past 1 (distance between particle centers == rope thickness). Is this the same resolution of the blueprint you originally created?. Also make sure the blueprint thickness is the same. Otherwise the amount of particles in the blueprint as well as the distance between them will be different.

Also keep in mind that Obi is a position-based engine. This means velocities are derived from positional deltas, you should set both solver.positions as well as solver.prevPositions to the same values, otherwise particle velocities will be non-zero at the start because velocity = (pos-prevpos) / time.

Quote:it can be observed that the particles extracted from rope.solverIndices are not contiguous in position, which I believe to be normal according to the Obi documentation. However, I am not certain if this is the cause of the loss of detail.

This will only happen if your ropes have had their length changed at runtime using a cursor, or if they have been cut. Otherwise order should be contiguous. If you're not doing either, then it means you're either storing or loading particles in the wrong order.

kind regards
Reply
#13
(27-06-2024, 12:22 PM)josemendez Wrote: You're setting the blueprint's resolution to 100. It can't go past 1 (distance between particle centers == rope thickness). Is this the same resolution of the blueprint you originally created?. Also make sure the blueprint thickness is the same. Otherwise the amount of particles in the blueprint as well as the distance between them will be different.
The blueprint's resolution should be 1; that was my mistake, a paste error.

(27-06-2024, 12:22 PM)josemendez Wrote: Also keep in mind that Obi is a position-based engine. This means velocities are derived from positional deltas, you should set both solver.positions as well as solver.prevPositions to the same values, otherwise particle velocities will be non-zero at the start because velocity = (pos-prevpos) / time.


This will only happen if your ropes have had their length changed at runtime using a cursor, or if they have been cut. Otherwise order should be contiguous. If you're not doing either, then it means you're either storing or loading particles in the wrong order.
I will try setting the prevPositions, indeed, I  didn't set them in the code.


Well, I did indeed change the length of the rope during runtime using the cursor. Previously, I had written a random algorithm to generate key points for the rope's path, and then I added these key points to the controlPoints to make the rope generate according to this path. This approach resulted in a very long rope, with excess length bunched up, but the interweaving between the ropes was indeed effective, so I had to contract it until all the ropes were tightened, at which point the ropes had no excess length and all maintained the randomly generated interweaving state. I am not sure if this approach is reasonable.  Random algorithm is really tough to me. (I am eager to receive some recommendations for good entanglement generation algorithms. Sonrojado Llorar )


(27-06-2024, 12:22 PM)josemendez Wrote: Also keep in mind that Obi is a position-based engine. This means velocities are derived from positional deltas, you should set both solver.positions as well as solver.prevPositions to the same values, otherwise particle velocities will be non-zero at the start because velocity = (pos-prevpos) / time.


This will only happen if your ropes have had their length changed at runtime using a cursor, or if they have been cut. Otherwise order should be contiguous. If you're not doing either, then it means you're either storing or loading particles in the wrong order.

kind regards
I have tried setting solver.positions and solver.prevPositions, but it didn't have much effect.

For now, it seems I have hit a deadlock. Contracting the rope to reduce the impact of the random path generation on it appears to be inevitable for me at this point.

(27-06-2024, 12:22 PM)josemendez Wrote: This will only happen if your ropes have had their length changed at runtime using a cursor, or if they have been cut. Otherwise order should be contiguous. If you're not doing either, then it means you're either storing or loading particles in the wrong order.

When I stored the particles before, I directly iterated through rope.solverIndices. If it's because I changed the length of the rope, which caused the order of the particles to be disrupted, Then rope.elements must be continuous. I tried only using rope.elements when storing, and assigning directly to solver.positions[particlesIndices[i]] during restore, but it still didn't work. The root cause is that the order in rope.solver.positions is disordered, right?
Reply
#14
(27-06-2024, 12:22 PM)josemendez Wrote: This will only happen if your ropes have had their length changed at runtime using a cursor, or if they have been cut. Otherwise order should be contiguous. If you're not doing either, then it means you're either storing or loading particles in the wrong order.
I apologize for disturbing you again. Previously, following your guidance, I saved and restored the control points, and initially achieved the recovery of the rope's shape. As the functionality has been developed over time, I've found that to be an illusion; only simple entanglement shapes can be recovered, and all slightly complex ones cannot be recovered.

Here are the complete steps I have taken with the rope:

1.Using an algorithm to directly generate entanglement points between the starting and ending points of the rope, and making multiple ropes entangle with each other. The starting point, endpoint, and entanglement points are added to the control points in order, then the rope is instantiated.
2.After the instantiation of the rope in the previous step, it may be because there are too many control points, at this point, the instantiated ropes are very long and bunched up, but it can be seen that there are effective entanglement points between the ropes. So I further contracted these ropes until they became tight. Due to the physical pull caused by entanglement between the ropes, these pull points mostly occur in the middle part of the ropes, so I set both cursorMu and SourceMu to 0.1, making the ropes as unaffected by the entanglement points as possible during the contraction.
3.After all the above ropes are completely generated and stationary, I stored all the necessary information of the ropes into JSON (we have discussed the details before). When starting a new level, read the data from JSON and restore the ropes.
As you said, changing the length of the rope during runtime may lead to an error in the order of particles, but now I can't avoid this operation unless I can find a suitable generation method so that the ropes generated in step 2 are just the right length.

Since your last suggestion, I have also tried the following:

1. I found that the order of rope.elements is continuous, so I tried to store this and restore it to solver.positions[particlesIndices[i]], but it did not work.
2. I tried to store the index of rope.solverIndices and the corresponding solver.positions[particlesIndices[i]] and restore them, but it did not work.

I don't have any other ways now.

I am an independent developer, and the rope gameplay can be said to be the core part, and most of the game is centered around the rope. If the loading of the rope is always not figured out, there is no need to do other game content. In the past two or three months, I have spent a lot of time on the debugging of the rope's physical properties and operational feel, as well as the writing of the code. I don't want to give up yet, and I don't want to waste this part of the time in vain. Perhaps you can give me some more ideas, really thank you very much.
Reply
#15
Hi!

Neither of these will work.

(11-07-2024, 07:21 AM)tapLucas Wrote: 1. I found that the order of rope.elements is continuous, so I tried to store this and restore it to solver.positions[particlesIndices[i]], but it did not work.

Storing particle data in the order dictated by elements isn't the same as storing particles in their original order and then accessing them in the order defined by elements. For starters you will be duplicating the amount of particles, and to make things worse the order in which particles are accessed will be broken. You need to store elements themselves, then restore the elements when loading the rope.

(11-07-2024, 07:21 AM)tapLucas Wrote: 2. I tried to store the index of rope.solverIndices and the corresponding solver.positions[particlesIndices[i]] and restore them, but it did not work.

This will ignore elements completely, so the order of particles will also be wrong.

Ropes are very similar to meshes: there's a list of vertex (particle) positions, and then a list of triangles (elements) that tell you how vertices/particles are attached to each other. You must store both if you want to keep the shape of the rope. For instance, imagine these are your particles:

A,B,C,D,E

and these your elements:

(0,2)(1,3)(3,4)

The resulting rope would be:

A--C  B--D--E

Notice how the order in which particles appear in the rope is different to the order in which they are stored, and how there's a cut in the rope (as particles C and B aren't connected by any element). There's 5 particles but only 3 elements. You need to reflect this in the data you store for your ropes, otherwise particles won't be properly connected. This can only be done by storing both particles and elements, altering the order in which you store the particles is not enough.

kind regards,
Reply
#16
(12-07-2024, 08:50 AM)josemendez Wrote: Notice how the order in which particles appear in the rope is different to the order in which they are stored, and how there's a cut in the rope (as particles C and B aren't connected by any element). There's 5 particles but only 3 elements. You need to reflect this in the data you store for your ropes, otherwise particles won't be properly connected. This can only be done by storing both particles and elements, altering the order in which you store the particles is not enough.

Hello, it's been a long time. I apologize that I have been too busy catching up on progress and developing new features to reply to the post.
Your suggestions to me were enlightening. I realized that my previous approach was too fixated on the internal logic of the rope, when in fact the simplest method is to describe the specific shape of the rope with a series of coordinates and rotations, and that's enough.

The day after reading your response, I successfully implemented the function to restore the specific shape of the rope from JSON data. In fact, stepping away from the work environment and thinking about the problem in a completely new environment or with a fresh mindset is more likely to lead to new discoveries.

Here are the specific steps for storing and restoring the shape of the rope using JSON, which I hope will be convenient for any developers in need and can help you all
  1. Save the restLength of the obi rope. This helps to describe the specific length the rope needs when restoring, which will also affect the space required for the position to take effect in the solver.

  2. Save the positions of the controlPoints. In my case, controlPoints mean some twist points that the rope must pass through in addition to the starting and ending points. For example, if you want to make the rope into an M shape, then there are a total of 5 controlPoints.

  3. Obtain the particle indices in the rope.elements, and then get the position and rotation of the particle in the solver, and it is very important to save in order, because this means a complete and coherent rope path.

  4. Generate a new instance of the obi rope in any scene, use controlPoints to define the shape of the rope, and generate enough particles.

  5. After generating a new rope instance, start the work of restoring data from JSON.

  6. Restore the restLength and call ChangeLength. This will ensure that the rope is adjusted to have enough length and space to accommodate the total number of particles needed to restore the shape of the rope.

  7. Restore the previously saved positions and rotations of the particles, and it is very important to read in order.

Here is the detailed code:

(17-08-2024, 09:02 AM)tapLucas Wrote: Here is the detailed code:
Save to Json:

Code:
    public JSONObject ToJson()
    {
        var restLength = gameObject.GetComponent<ObiRope>().restLength;
        jsonObject.AddField("restLength", restLength);
        jsonObject.AddField("particles", GetRopeParticleData());
        jsonObject.AddField("controlPoints", GetRopeControlPoints());
        return jsonObject;
    }

    JSONObject GetRopeControlPoints()
    {
        var rope = gameObject.GetComponent<ObiRope>();
        var pointsArray = JSONObject.emptyArray;
        foreach (var obiWingedPoint in rope.ropeBlueprint.path.m_Points.data)
        {
            pointsArray.Add((JSONNode)obiWingedPoint.position);
        }
        var jsonObject = JSONObject.emptyObject;
        jsonObject.AddField("points", pointsArray);
        return jsonObject;
    }
   
    JSONObject GetRopeParticleData()
    {
        var rope = gameObject.GetComponent<ObiRope>();
        var solver = rope.solver;
        var elementsArray = JSONObject.emptyArray;
        foreach (var element in rope.elements)
        {
            var json = JSONObject.emptyObject;
            json.AddField("particle1", (JSONNode)solver.positions[element.particle1]);
            json.AddField("particle2", (JSONNode)solver.positions[element.particle2]);
            json.AddField("restLength", element.restLength);
            elementsArray.Add(json);
        }

        var ropeObject = JSONObject.emptyObject;
        ropeObject.AddField("elements", elementsArray);
        return ropeObject;
    }


Instantiate rope by controlPoints:


Code:
private List<ObiWingedPoint> GetControlPoints(JSONObject ropeJsonObject)

    {
        var controlPoints = ropeJsonObject["controlPoints"];
        var pointsArray = controlPoints["points"];
       
        List<ObiWingedPoint> Points = new List<ObiWingedPoint>();
        if (pointsArray != null)
        {
            for (var i = 0; i < pointsArray.list.Count; i++)
            {
                var position = JSONNode.ToVector3_Short(pointsArray[i]);
                Points.Add(new ObiWingedPoint(Vector3.zero, position, Vector3.zero));
            }
        }
        return Points;
    }

    private GameObject InstantiateRope(GameObject objectToAttach1, GameObject objectToAttach2, List<ObiWingedPoint> controlPoints)
    {
        var(rpObject, isNewCreate) = ObjectPool.Instance.GetRopeFromPool();
        ObiRope rope = rpObject.GetComponent<ObiRope>();
       
        Transform transformA = objectToAttach1.transform;
        Transform transformB = objectToAttach2.transform;
        Vector3 positionA = transformA.position;
        Vector3 positionB = transformB.position;
        Vector3 objectScale = transformA.localScale;
        Vector3 offset = new Vector3(0, 0.05f, 0);

        Vector3 startPositionLS = transform.InverseTransformPoint(positionA + offset);
        Vector3 endPositionLS = transform.InverseTransformPoint(positionB + offset);
        //Vector3 tangentLS = (endPositionLS - startPositionLS).normalized;
        Vector3 tangentLS = Vector3.zero;

        ObiRopeBlueprint blueprint = ScriptableObject.CreateInstance<ObiRopeBlueprint>();
        blueprint.path.Clear();
        blueprint.pooledParticles = 50;
        blueprint.resolution = blueprintResolutionDefault;
     
        // Build the rope path:
        int filter = ObiUtils.MakeFilter(ObiUtils.CollideWithEverything, 0);
     
        int posIndex = 0;
        foreach (var obiWingedPoint in controlPoints)
        {
            string pointName = $"pos_{posIndex}";
            if (posIndex == 0)
            {
                pointName = "start";
            }
            else if (posIndex == controlPoints.Count - 1)
            {
                pointName = "end";
            }

            blueprint.path.AddControlPoint(obiWingedPoint.position, obiWingedPoint.inTangent, obiWingedPoint.outTangent,
                Vector3.up, ropeMass, 0.1f, ropeThickness, filter, Color.yellow, pointName);
            posIndex++;
        }
       
      
        blueprint.path.FlushEvents();

        // Generate particles/constraints:
        //blueprint.GenerateImmediate();
       
        if (!isNewCreate)
        {
            var attachComps = rpObject.GetComponents<ObiParticleAttachment>();
            foreach (var comp in attachComps)
            {
                Destroy(comp);
            }
        }
       
        ObiParticleAttachment attachment1 = rpObject.AddComponent<ObiParticleAttachment>();
        ObiParticleAttachment attachment2 = rpObject.AddComponent<ObiParticleAttachment>();

        // Set the blueprint:
        rope.ropeBlueprint = blueprint;
       
        // Attach both ends:
        attachment1.target = transformA;
        attachment2.target = transformB;
        attachment1.particleGroup = blueprint.groups[0];
        attachment2.particleGroup = blueprint.groups.Last();

        // Parent the actor under a solver to start the simulation:
        rpObject.transform.SetParent(obiSolverObject.transform);
       
        return rpObject;
    }

After instantiation, call FromJson:

Code:
private JSONObject particleDataToRestore;
private float ropeLength;
  public void FromJson(JSONObject jsonObject)
    {
     
        ropeLength = jsonObject["restLength"].floatValue;
        particleDataToRestore = jsonObject["particles"];
        StartCoroutine(PostFromJson());
    }
   
    IEnumerator PostFromJson()
    {
        yield return new WaitForEndOfFrame(); // may not necessary
        gameObject.GetComponent<ObiRopeCursor>().ChangeLength(RopeLength);
        yield return new WaitForEndOfFrame(); // may not necessary
        SetElementsPosition(gameObject.GetComponent<ObiRope>());
    }
   
   
    void SetElementsPosition(ObiActor actor)
    {
        var solver = actor.solver;
        var rope = actor as ObiRope;

        var elementsArray = particleDataToRestore["elements"];
        if (elementsArray != null && elementsArray.type == JSONObject.Type.Array)
        {
            int elementCount = elementsArray.list.Count;
            for (var i = 0; i < elementCount; i++)
            {
                var element = elementsArray[i];
               
                if (i >= rope.elements.Count)
                {
                    Debug.LogWarning("Out of Range Exception ! ");
                    continue;
                }

                var restLength = float.Parse(element["restLength"].stringValue);
                rope.elements[i].restLength = restLength;
                rope.elements[i].constraintForce = 0;
                rope.elements[i].tearResistance = 1;

                var particle1 = JSONNode.ToVector3_Short(element["particle1"]);
                var particle2 = JSONNode.ToVector3_Short(element["particle2"]);

                solver.positions[rope.elements[i].particle1] = particle1;
                solver.positions[rope.elements[i].particle2] = particle2;

                solver.prevPositions[rope.elements[i].particle1] = particle1;
                solver.prevPositions[rope.elements[i].particle2] = particle2;
               
                solver.velocities[rope.elements[i].particle1] = Vector4.zero;
                solver.velocities[rope.elements[i].particle2] = Vector4.zero;
               
                solver.angularVelocities[rope.elements[i].particle1] = Vector4.zero;
                solver.angularVelocities[rope.elements[i].particle2] = Vector4.zero;
           
            }
        }
    }


Unfortunately, at present, the primary target platform for my project is WEBGL. After I packaged the test project, I found that the running efficiency was extremely poor and the frame rate was very low. During the first project survey, after implementing the obi rope plugin, I had packaged a test project once, and at that time there were only 2 ropes in the scene, and I mistakenly thought it was due to the data and jobs not being well configured. After several days of continuous attempts and data collection, I determined that the obi rope is not suitable for WEBGL, and I have to start over with the rope code. (At least as of today, August 17, 2024.)
Llorar
Reply
#17
(17-08-2024, 09:02 AM)tapLucas Wrote:
Unfortunately, at present, the primary target platform for my project is WEBGL. After I packaged the test project, I found that the running efficiency was extremely poor and the frame rate was very low. During the first project survey, after implementing the obi rope plugin, I had packaged a test project once, and at that time there were only 2 ropes in the scene, and I mistakenly thought it was due to the data and jobs not being well configured. After several days of continuous attempts and data collection, I determined that the obi rope is not suitable for WEBGL, and I have to start over with the rope code. (At least as of today, August 17, 2024.)
Llorar

Hi!

WebGL is not supported by Burst, as per the manual:
https://docs.unity3d.com/Packages/com.un...pport.html

Also see:
https://discussions.unity.com/t/burst-for-webgl/849368

Sorry you had to waste so much time on this, had I known beforehand that you were targeting WebGL I would have told you straight away it wasn’t possible. Any Burst/Jobs code will be executed as regular (unvectorized) code in the main thread when running WebGL, and as a result it will run a lot slower.

We can’t do anything regarding this ourselves, as we don’t develop or maintain the Burst compiler. It’s up to Unity to add support for WebGL, and at the time it is unclear when (or if) they’ll add it.

kind regards,
Reply
#18
Yes, due to the limitations of WEBGL, it doesn't support Burst and multithreading. In any case, I will look for new solutions. You are the best development consultant, and I am very grateful for your help!   Guiño Gran sonrisa
Reply
#19
(17-08-2024, 10:37 AM)tapLucas Wrote: Yes, due to the limitations of WEBGL, it doesn't support Burst and multithreading. In any case, I will look for new solutions. You are the best development consultant, and I am very grateful for your help!   Guiño Gran sonrisa

Thanks! It’s my pleasure to help developers like yourself Sonrisa

I was thinking that maybe Obi7 would help since it has a fully GPU, compute shader-based backend. However it turns out compute shaders aren’t supoorted in WebGL either. :/

Other similar assets I know of (Rope Toolkit/Minikit) are also based on Burst, so they’ll have the same limitation.

Off the top of my head, your best bet is to use either Unity’s articulated bodies or rigidbodies+joints, and develop a custom system based around them. It’s certainly a time investment, but has the best chance to work reasonably well in WebGL imho.

Best of luck with your project,

Kind regards
Reply
#20
(17-08-2024, 10:46 AM)josemendez Wrote: Thanks! It’s my pleasure to help developers like yourself Sonrisa

I was thinking that maybe Obi7 would help since it has a fully GPU, compute shader-based backend. However it turns out compute shaders aren’t supoorted in WebGL either. :/

Other similar assets I know of (Rope Toolkit/Minikit) are also based on Burst, so they’ll have the same limitation.
If everything goes smoothly, perhaps when I release the APK or iOS version in the future, I will switch back to OBI, because the simulation effect is the best among other solutions.



(17-08-2024, 10:46 AM)josemendez Wrote: Off the top of my head, your best bet is to use either Unity’s articulated bodies or rigidbodies+joints, and develop a custom system based around them. It’s certainly a time investment, but has the best chance to work reasonably well in WebGL imho.

Best of luck with your project,

Kind regards
Such advice is really great, thank you very much. In my view, the performance of WEBGL is only about 1/3 of that of ordinary platforms. The joint in Unity can simulate the rope, but it will still be choppy when there are too many ropes or too many iterations of the solver. It has been so long since last time, and I have indeed been trying in this area and have made some progress. Although the simulation effect is not as good as OBI, it is still usable for now.

Thank you again for your sincere help, it means a lot to me. I wish you all the best in your work! Guiño
Reply