The video you sent causes VLC, Quicktime and Media Player to crash, so I'm unable to see it.
When setting positions directly you also need to set the inverse mass and the velocity of these particles to zero, as I pointed out before. Setting the inverse mass to zero deactivates dynamics for that particle (as you will be setting its position directly, and you don't want the simulation to overwrite it), and removing all its velocity will make sure it doesn't drift away from the position you set.
Once you "release" the particle, simply set its inverse mass to whatever it was. The simulation will take over and begin setting both its position and velocity.
transform.position assign attempt for 'Dragon' is not valid. Input position is { NaN, NaN, NaN }.
UnityEngine.Transformet_position(Vector3)
Obi.ObiSoftbody:OnSolverStepEnd(Single) (at Assets/Obi/Scripts/Actors/ObiSoftbody.cs:65)
Obi.ObiSolver:AllSolversStepEnd() (at Assets/Obi/Scripts/Solver/ObiSolver.cs:877)
Obi.ObiArbiter:WaitForAllSolvers() (at Assets/Obi/Scripts/Solver/ObiArbiter.cs:45)
Obi.ObiSolver:WaitForAllSolvers() (at Assets/Obi/Scripts/Solver/ObiSolver.cs:784)
Obi.ObiSolver:SimulateStep(Single) (at Assets/Obi/Scripts/Solver/ObiSolver.cs:672)
Obi.ObiSolver:FixedUpdate() (at Assets/Obi/Scripts/Solver/ObiSolver.cs:866)
And multiple clusters errors like:
transform.position assign attempt for 'Cluster102' is not valid. Input position is { NaN, NaN, NaN }.
UnityEngine.Transformet_position(Vector3)
Obi.ObiSoftbodySkinner:UpdateBones(Object, EventArgs) (at Assets/Obi/Rendering/ObiSoftbodySkinner.cs:122)
Obi.ObiSolver:EndFrame(Single) (at Assets/Obi/Scripts/Solver/ObiSolver.cs:727)
Obi.ObiSolver:LateUpdate() (at Assets/Obi/Scripts/Solver/ObiSolver.cs:905)
transform.position assign attempt for 'Dragon' is not valid. Input position is { NaN, NaN, NaN }.
UnityEngine.Transformet_position(Vector3)
Obi.ObiSoftbody:OnSolverStepEnd(Single) (at Assets/Obi/Scripts/Actors/ObiSoftbody.cs:65)
Obi.ObiSolver:AllSolversStepEnd() (at Assets/Obi/Scripts/Solver/ObiSolver.cs:877)
Obi.ObiArbiter:WaitForAllSolvers() (at Assets/Obi/Scripts/Solver/ObiArbiter.cs:45)
Obi.ObiSolver:WaitForAllSolvers() (at Assets/Obi/Scripts/Solver/ObiSolver.cs:784)
Obi.ObiSolver:SimulateStep(Single) (at Assets/Obi/Scripts/Solver/ObiSolver.cs:672)
Obi.ObiSolver:FixedUpdate() (at Assets/Obi/Scripts/Solver/ObiSolver.cs:866)
And multiple clusters errors like:
transform.position assign attempt for 'Cluster102' is not valid. Input position is { NaN, NaN, NaN }.
UnityEngine.Transformet_position(Vector3)
Obi.ObiSoftbodySkinner:UpdateBones(Object, EventArgs) (at Assets/Obi/Rendering/ObiSoftbodySkinner.cs:122)
Obi.ObiSolver:EndFrame(Single) (at Assets/Obi/Scripts/Solver/ObiSolver.cs:727)
Obi.ObiSolver:LateUpdate() (at Assets/Obi/Scripts/Solver/ObiSolver.cs:905)
The dragon disappears.
Btw, the examples from the manual do nothing
Hi there,
Going to write an example script and post it here, for reference.
public UnityEngine.Events.UnityEvent OnParticlePicked;
public UnityEngine.Events.UnityEvent OnParticleDragged;
public UnityEngine.Events.UnityEvent OnParticleReleased;
public UnityEngine.Events.UnityEvent OnParticlePicked;
public UnityEngine.Events.UnityEvent OnParticleDragged;
public UnityEngine.Events.UnityEvent OnParticleReleased;
Whenever you manually change the inverse mass (and thus, the mass) of a particle involved in a shape matching constraint, you need to recalculate the shape matching cluster's rest state. I really should have pointed this out before, kinda took it for granted it but it's not entirely obvious. My apologies.
Lastly, it only works for 1 particle (like the default dragger). There's cases where you want to grab multiple particles, but well that's just an additional bonus.
My next post contains a complete example that works via contact callbacks, so you can grab multiple particles that are in touch with a collider (or inside a trigger, for that matter).
14-11-2019, 12:52 PM (This post was last modified: 14-11-2019, 01:09 PM by josemendez.)
Here's a complete example with all the bells and whistles. You can also find it attached for convenience.
Code:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using Obi;
/**
* Sample component that makes a collider "grab" any particle it touches (regardless of which Actor it belongs to).
*/
[RequireComponent(typeof(ObiCollider))]
public class Grabber : MonoBehaviour
{
public ObiSolver solver;
/**
* Helper class that stores the index of a particle in the solver, its position in the grabber's local space, and its inverse mass previous to being grabbed.
* This makes it easy to tell if a particle has been grabbed, update its position while grabbing, and restore its mass after being released.
*/
private class GrabbedParticle : IEqualityComparer<GrabbedParticle>
{
public int index;
public float invMass;
public Vector3 localPosition;
public int GetHashCode(GrabbedParticle obj)
{
return index;
}
}
private Obi.ObiSolver.ObiCollisionEventArgs collisionEvent; /**< store the current collision event*/
private ObiCollider localCollider; /**< the collider on this gameObject.*/
private HashSet<GrabbedParticle> grabbedParticles = new HashSet<GrabbedParticle>(); /**< set to store all currently grabbed particles.*/
private HashSet<ObiSoftbody> grabbedSoftbodies = new HashSet<ObiSoftbody>(); /**< set of softbodies grabbed during this step.*/
private Matrix4x4 grabber2Solver;
private Matrix4x4 solver2Grabber;
private void Solver_OnCollision(object sender, Obi.ObiSolver.ObiCollisionEventArgs e)
{
// Calculate transform matrix from grabber to world space (Note: if using local space simulation, postmultiply with solver.transform.localToWorldMatrix)
solver2Grabber = transform.worldToLocalMatrix;
// and its inverse:
grabber2Solver = solver2Grabber.inverse;
collisionEvent = e;
}
private void UpdateRestShapeMatching()
{
// Update rest shape matching of all grabbed softbodies:
foreach (ObiSoftbody softbody in grabbedSoftbodies)
foreach (ObiShapeMatchingConstraintBatch batch in softbody.ShapeMatchingConstraints.GetBatches())
Oni.CalculateRestShapeMatching(solver.OniSolver, batch.OniBatch);
}
/**
* Creates and stores a GrabbedParticle from the particle at the given index.
* Returns true if we sucessfully grabbed a particle, false if the particle was already grabbed.
*/
private bool GrabParticle(int index)
{
GrabbedParticle p = new GrabbedParticle(index, solver.invMasses[index]);
// in case this particle has not been grabbed yet:
if (!grabbedParticles.Contains(p))
{
// record the particle's position relative to the grabber, and store it.
p.localPosition = solver2Grabber.MultiplyPoint3x4(solver.positions[index]);
grabbedParticles.Add(p);
// Set inv mass and velocity to zero:
solver.invMasses[index] = 0;
solver.velocities[index] = Vector4.zero;
return true;
}
return false;
}
/**
* Grabs all particles currently touching the grabber.
*/
public void Grab()
{
grabbedSoftbodies.Clear();
foreach (Oni.Contact contact in collisionEvent.contacts)
{
// this one is an actual collision:
if (contact.distance < 0.01f)
{
Component contactCollider;
if (ObiCollider.idToCollider.TryGetValue(contact.other, out contactCollider))
{
// if the current contact references our collider, proceed to grab the particle.
if (contactCollider == localCollider.SourceCollider)
{
// try to grab the particle, if not already grabbed.
if (GrabParticle(contact.particle))
{
// we want to know if we grabbed a softbody, to update its rest shape.
var softbody = solver.particleToActor[contact.particle].actor as ObiSoftbody;
if (softbody != null)
grabbedSoftbodies.Add(softbody);
}
}
}
}
}
UpdateRestShapeMatching();
}
/**
* Releases all currently grabbed particles. This boils down to simply resetting their invMass.
*/
public void Release()
{
// Restore the inverse mass of all grabbed particles, so dynamics affect them.
foreach (GrabbedParticle p in grabbedParticles)
solver.invMasses[p.index] = p.invMass;
grabbedParticles.Clear();
// Also update rest shape matching:
UpdateRestShapeMatching();
grabbedSoftbodies.Clear();
}
/**
* Updates the position of the grabbed particles.
*/
private void FixedUpdate()
{
foreach (GrabbedParticle p in grabbedParticles)
solver.positions[p.index] = grabber2Solver.MultiplyPoint3x4(p.localPosition);
}
/**
* Just for convenience. Ideally, this should not be part of this component.
* You're expected to control the Grabber from outside.
*/
public void Update()
{
if (Input.GetKeyDown(KeyCode.G))
{
Grab();
}
if (Input.GetKeyDown(KeyCode.R))
{
Release();
}
}
}
It is a component that you can add to any ObiCollider, and it takes a reference to a ObiSolver as input.
It has two methods: Grab() and Release():
- Grab() detects all particles in touch with the collider, stores them in a set together with their invMass and local space position. Then sets its invMass and velocity to zero. Also updates rest shape matching in case any of the particles belonged to a softbody.
- Release() resets all the currently grabbed particle's invMass to whatever it was before grabbing them. This makes them be driven by dynamics once again.
I've included code in Update() that calls Grab() upon pressing "G" in the keyboard, and Release() when pressing "R", for demonstration purposes. Modify it to your taste.
(14-11-2019, 12:52 PM)josemendez Wrote: Here's a complete example with all the bells and whistles. You can also find it attached for convenience.
Code:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using Obi;
/**
* Sample component that makes a collider "grab" any particle it touches (regardless of which Actor it belongs to).
*/
[RequireComponent(typeof(ObiCollider))]
public class Grabber : MonoBehaviour
{
public ObiSolver solver;
/**
* Helper class that stores the index of a particle in the solver, its position in the grabber's local space, and its inverse mass previous to being grabbed.
* This makes it easy to tell if a particle has been grabbed, update its position while grabbing, and restore its mass after being released.
*/
private class GrabbedParticle : IEqualityComparer<GrabbedParticle>
{
public int index;
public float invMass;
public Vector3 localPosition;
public int GetHashCode(GrabbedParticle obj)
{
return index;
}
}
private Obi.ObiSolver.ObiCollisionEventArgs collisionEvent; /**< store the current collision event*/
private ObiCollider localCollider; /**< the collider on this gameObject.*/
private HashSet<GrabbedParticle> grabbedParticles = new HashSet<GrabbedParticle>(); /**< set to store all currently grabbed particles.*/
private HashSet<ObiSoftbody> grabbedSoftbodies = new HashSet<ObiSoftbody>(); /**< set of softbodies grabbed during this step.*/
private Matrix4x4 grabber2Solver;
private Matrix4x4 solver2Grabber;
private void Solver_OnCollision(object sender, Obi.ObiSolver.ObiCollisionEventArgs e)
{
// Calculate transform matrix from grabber to world space (Note: if using local space simulation, postmultiply with solver.transform.localToWorldMatrix)
solver2Grabber = transform.worldToLocalMatrix;
// and its inverse:
grabber2Solver = solver2Grabber.inverse;
collisionEvent = e;
}
private void UpdateRestShapeMatching()
{
// Update rest shape matching of all grabbed softbodies:
foreach (ObiSoftbody softbody in grabbedSoftbodies)
foreach (ObiShapeMatchingConstraintBatch batch in softbody.ShapeMatchingConstraints.GetBatches())
Oni.CalculateRestShapeMatching(solver.OniSolver, batch.OniBatch);
}
/**
* Creates and stores a GrabbedParticle from the particle at the given index.
* Returns true if we sucessfully grabbed a particle, false if the particle was already grabbed.
*/
private bool GrabParticle(int index)
{
GrabbedParticle p = new GrabbedParticle(index, solver.invMasses[index]);
// in case this particle has not been grabbed yet:
if (!grabbedParticles.Contains(p))
{
// record the particle's position relative to the grabber, and store it.
p.localPosition = solver2Grabber.MultiplyPoint3x4(solver.positions[index]);
grabbedParticles.Add(p);
// Set inv mass and velocity to zero:
solver.invMasses[index] = 0;
solver.velocities[index] = Vector4.zero;
return true;
}
return false;
}
/**
* Grabs all particles currently touching the grabber.
*/
public void Grab()
{
grabbedSoftbodies.Clear();
foreach (Oni.Contact contact in collisionEvent.contacts)
{
// this one is an actual collision:
if (contact.distance < 0.01f)
{
Component contactCollider;
if (ObiCollider.idToCollider.TryGetValue(contact.other, out contactCollider))
{
// if the current contact references our collider, proceed to grab the particle.
if (contactCollider == localCollider.SourceCollider)
{
// try to grab the particle, if not already grabbed.
if (GrabParticle(contact.particle))
{
// we want to know if we grabbed a softbody, to update its rest shape.
var softbody = solver.particleToActor[contact.particle].actor as ObiSoftbody;
if (softbody != null)
grabbedSoftbodies.Add(softbody);
}
}
}
}
}
UpdateRestShapeMatching();
}
/**
* Releases all currently grabbed particles. This boils down to simply resetting their invMass.
*/
public void Release()
{
// Restore the inverse mass of all grabbed particles, so dynamics affect them.
foreach (GrabbedParticle p in grabbedParticles)
solver.invMasses[p.index] = p.invMass;
grabbedParticles.Clear();
// Also update rest shape matching:
UpdateRestShapeMatching();
grabbedSoftbodies.Clear();
}
/**
* Updates the position of the grabbed particles.
*/
private void FixedUpdate()
{
foreach (GrabbedParticle p in grabbedParticles)
solver.positions[p.index] = grabber2Solver.MultiplyPoint3x4(p.localPosition);
}
/**
* Just for convenience. Ideally, this should not be part of this component.
* You're expected to control the Grabber from outside.
*/
public void Update()
{
if (Input.GetKeyDown(KeyCode.G))
{
Grab();
}
if (Input.GetKeyDown(KeyCode.R))
{
Release();
}
}
}
It is a component that you can add to any ObiCollider, and it takes a reference to a ObiSolver as input.
It has two methods: Grab() and Release():
- Grab() detects all particles in touch with the collider, stores them in a set together with their invMass and local space position. Then sets its invMass and velocity to zero. Also updates rest shape matching in case any of the particles belonged to a softbody.
- Release() resets all the currently grabbed particle's invMass to whatever it was before grabbing them. This makes them be driven by dynamics once again.
I've included code in Update() that calls Grab() upon pressing "G" in the keyboard, and Release() when pressing "R", for demonstration purposes. Modify it to your taste.
Edit: a video of it in action:
Awesome! Thanks! Could you add it to the store package?