using UnityEngine; using System; using System.Collections; using System.Collections.Generic; namespace Obi { /** * Represents a group of related particles. ObiActor does not make * any assumptions about the relationship between these particles, except that they get allocated * and released together. */ [ExecuteInEditMode] [DisallowMultipleComponent] public abstract class ObiActor : MonoBehaviour, IObiParticleCollection { public class ObiActorSolverArgs : System.EventArgs { public ObiSolver solver { get; } public ObiActorSolverArgs(ObiSolver solver) { this.solver = solver; } } private struct BufferedForces { public bool dirty; public Vector4 force; public Vector4 acceleration; public Vector4 impulse; public Vector4 velChange; public Vector4 angularForce; public Vector4 angularAcceleration; public Vector4 angularImpulse; public Vector4 angularVelChange; public void Clear() { force = Vector4.zero; acceleration = Vector4.zero; impulse = Vector4.zero; velChange = Vector4.zero; angularForce = Vector4.zero; angularAcceleration = Vector4.zero; angularImpulse = Vector4.zero; angularVelChange = Vector4.zero; } } public delegate void ActorCallback(ObiActor actor); public delegate void ActorStepCallback(ObiActor actor, float simulatedTime, float substepTime); public delegate void ActorBlueprintCallback(ObiActor actor, ObiActorBlueprint blueprint); /// /// Called when the actor blueprint has been loaded into the solver. /// public event ActorBlueprintCallback OnBlueprintLoaded; /// /// Called when the actor blueprint has been unloaded from the solver. /// public event ActorBlueprintCallback OnBlueprintUnloaded; /// /// Called when the blueprint currently in use has been re-generated. This will always be preceded by a call to /// OnBlueprintUnloaded and OnBlueprintLoaded. /// public event ActorBlueprintCallback OnBlueprintRegenerated; /// /// Called before simulation starts. /// public event ActorStepCallback OnSimulationStart; /// /// Called after CPU->GPU data transfers, before collision detection starts. /// public event ActorStepCallback OnCollisionDetectionStart; /// /// Called before performing substepping. /// public event ActorStepCallback OnSubstepsStart; /// /// Called after simulation ends. /// public event ActorStepCallback OnSimulationEnd; /// /// You can use this callback to issue GPU->CPU readbacks. /// public event ActorCallback OnRequestReadback; /// /// Called at the end of each frame, after interpolation but before rendering. /// public event ActorStepCallback OnInterpolate; [HideInInspector] protected ObiNativeIntList m_ActiveParticleCount; /// /// Index of each one of the actor's particles in the solver. /// [HideInInspector] public ObiNativeIntList solverIndices; /// /// For each of the actor's constraint types, offset of every batch in the solver. /// [HideInInspector] public List[] solverBatchOffsets; public int deformableEdgesOffset { protected set; get; } /**< index of the first deformable edge in the solver that belongs to this rope.*/ protected ObiSolver m_Solver; protected bool m_Loaded = false; public int groupID = 0; private ObiActorBlueprint m_State; private ObiActorBlueprint m_BlueprintInstance; private ObiPinConstraintsData m_PinConstraints; private ObiPinholeConstraintsData m_PinholeConstraints; private BufferedForces bufferedForces = new BufferedForces(); [SerializeField] [HideInInspector] protected ObiCollisionMaterial m_CollisionMaterial; [SerializeField] [HideInInspector] protected bool m_SurfaceCollisions = false; [SerializeField] [HideInInspector] [Min(ObiUtils.epsilon)] protected float m_MassScale = 1; /// /// The solver in charge of simulating this actor. /// /// This is the first ObiSolver component found up the actor's hierarchy. public ObiSolver solver { get { return m_Solver; } } /// /// True if the actor blueprint has been loaded into a solver. /// If true, it guarantees actor.solver, actor.solverIndices and actor.solverBatchOffsets won't be null. /// public bool isLoaded { get { return m_Solver != null && m_Loaded; } } /// /// The collision material being used by this actor. /// public ObiCollisionMaterial collisionMaterial { get { return m_CollisionMaterial; } set { if (m_CollisionMaterial != value) { m_CollisionMaterial = value; UpdateCollisionMaterials(); } } } /// /// Whether to use simplices (triangles, edges) for contact generation. /// public virtual bool surfaceCollisions { get { return m_SurfaceCollisions; } set { if (value != m_SurfaceCollisions) { m_SurfaceCollisions = value; if (m_Solver != null) { m_Solver.dirtySimplices |= simplexTypes; } } } } /// /// Scale applied to this actor's particle masses. /// public float massScale { get { return m_MassScale; } set { if (Mathf.Abs(m_MassScale - value) > ObiUtils.epsilon) SetMassScale(value); } } /// /// Amount of particles allocated by this actor. /// public int particleCount { get { return sourceBlueprint != null ? sourceBlueprint.particleCount : 0; } } /// /// Amount of particles in use by this actor. /// /// This will always be equal to or smaller than . public int activeParticleCount { get { return m_ActiveParticleCount != null ? m_ActiveParticleCount[0]:0; } } /// /// Buffer of size 1 that contains the amount of active particles in use by this actor. /// Useful to modify the amount of active particles from a compute shader. /// /// This will always be equal to or smaller than . public ObiNativeIntList activeParticleCountBuffer { get { return m_ActiveParticleCount; } } /// /// Whether this actors makes use of particle orientations or not. /// public bool usesOrientedParticles { get { return sourceBlueprint != null && sourceBlueprint.invRotationalMasses != null && sourceBlueprint.invRotationalMasses.Length > 0 && sourceBlueprint.orientations != null && sourceBlueprint.orientations.Length > 0 && sourceBlueprint.restOrientations != null && sourceBlueprint.restOrientations.Length > 0; } } /// /// If true, it means particles may not be completely spherical, but ellipsoidal. /// public virtual bool usesAnisotropicParticles { get { return false; } } public Oni.SimplexType simplexTypes { get { return (sourceBlueprint != null) ? sourceBlueprint.simplexTypes : Oni.SimplexType.Point; } } /// /// Matrix that transforms from the actor's local space to the solver's local space. /// /// If there's no solver present, this is the same as the actor's local to world matrix. public Matrix4x4 actorLocalToSolverMatrix { get { if (m_Solver != null) return m_Solver.transform.worldToLocalMatrix * transform.localToWorldMatrix; else return transform.localToWorldMatrix; } } /// /// Matrix that transforms from the solver's local space to the actor's local space. /// /// If there's no solver present, this is the same as the actor's world to local matrix. /// This is always the same as the inverse of . public Matrix4x4 actorSolverToLocalMatrix { get { if (m_Solver != null) return transform.worldToLocalMatrix * m_Solver.transform.localToWorldMatrix; else return transform.worldToLocalMatrix; } } /// /// Reference to the blueprint asset used by this actor. /// public abstract ObiActorBlueprint sourceBlueprint { get; } /// /// Reference to the blueprint in use by this actor. /// /// If you haven't called before, this will be the same as . /// If you have called before, it will be the same as . public ObiActorBlueprint sharedBlueprint { get { if (m_BlueprintInstance != null) return m_BlueprintInstance; return sourceBlueprint; } } /// /// Returns a unique instance of this actor's . /// /// This is mostly used when the actor needs to change some blueprint data at runtime, /// and you don't want to change the blueprint asset as this would affect all other actors using it. Tearable cloth and ropes /// make use of this. public ObiActorBlueprint blueprint { get { if (m_BlueprintInstance == null && sourceBlueprint != null) m_BlueprintInstance = Instantiate(sourceBlueprint); return m_BlueprintInstance; } } protected virtual void Awake() { m_ActiveParticleCount = new ObiNativeIntList(); m_ActiveParticleCount.Add(0); #if UNITY_EDITOR // Check if this script's GameObject is in a PrefabStage #if UNITY_2021_2_OR_NEWER var prefabStage = UnityEditor.SceneManagement.PrefabStageUtility.GetPrefabStage(gameObject); #else var prefabStage = UnityEditor.Experimental.SceneManagement.PrefabStageUtility.GetPrefabStage(gameObject); #endif if (prefabStage != null) { // Only create a solver if there's not one up our hierarchy. if (GetComponentInParent() == null) { // Add our own environment root and move it to the PrefabStage scene var newParent = new GameObject("ObiSolver (Environment)", typeof(ObiSolver)); UnityEngine.SceneManagement.SceneManager.MoveGameObjectToScene(newParent, gameObject.scene); transform.root.parent = newParent.transform; } } #endif } protected virtual void OnDestroy() { m_ActiveParticleCount.Dispose(); m_ActiveParticleCount = null; if (m_BlueprintInstance != null) DestroyImmediate(m_BlueprintInstance); } protected virtual void OnEnable() { solverBatchOffsets = new List[Oni.ConstraintTypeCount]; for (int i = 0; i < solverBatchOffsets.Length; ++i) solverBatchOffsets[i] = new List(); m_PinConstraints = new ObiPinConstraintsData(); m_PinholeConstraints = new ObiPinholeConstraintsData(); // when an actor is enabled, grabs the first solver up its hierarchy, // initializes it (if not initialized) and gets added to it. m_Solver = GetComponentInParent(); AddToSolver(); } protected virtual void OnDisable() { RemoveFromSolver(); } protected virtual void OnValidate() { UpdateCollisionMaterials(); } private void OnTransformParentChanged() { if (isActiveAndEnabled) SetSolver(GetComponentInParent()); } /// /// Adds this actor to its solver, if any. Automatically called by . /// public void AddToSolver() { if (m_Solver != null) { if (sourceBlueprint != null) sourceBlueprint.OnBlueprintGenerate += OnBlueprintRegenerate; m_Solver.AddActor(this); } } /// /// Remove this actor from its solver, if any. Automatically called by . /// public void RemoveFromSolver() { if (m_Solver != null) { if (sourceBlueprint != null) sourceBlueprint.OnBlueprintGenerate -= OnBlueprintRegenerate; m_Solver.RemoveActor(this); } } /// /// Forcibly changes the solver in charge of this actor /// /// The solver we want to put in charge of this actor. /// First it removes the actor from its current solver, then changes the actor's current solver and then readds it to this new solver. protected void SetSolver(ObiSolver newSolver) { // In case the first solver up our hierarchy is not the one we're currently in, change solver. if (newSolver != m_Solver) { RemoveFromSolver(); m_Solver = newSolver; AddToSolver(); } } /// /// Sets the mass of all particles in the actor to their blueprint values, multiplied by a scale factor. /// /// new mass scale. protected void SetMassScale(float scale) { if (Application.isPlaying && isLoaded && particleCount > 0) { scale = Mathf.Max(ObiUtils.epsilon, scale); for (int i = 0; i < particleCount; ++i) { int solverIndex = solverIndices[i]; if (m_Solver.invMasses[solverIndex] > 0) m_Solver.invMasses[solverIndex] = sharedBlueprint.invMasses[i] / scale; if (m_Solver.invRotationalMasses[solverIndex] > 0 && sharedBlueprint.invRotationalMasses != null && i < sharedBlueprint.invRotationalMasses.Length) m_Solver.invRotationalMasses[solverIndex] = sharedBlueprint.invRotationalMasses[i] / scale; } UpdateParticleProperties(); } } protected virtual void OnBlueprintRegenerate(ObiActorBlueprint blueprint) { // Reload by removing the current blueprint from the solver, // destroying the current blueprint instance if any, // then adding the shared blueprint again. RemoveFromSolver(); if (m_BlueprintInstance != null) DestroyImmediate(m_BlueprintInstance); AddToSolver(); OnBlueprintRegenerated?.Invoke(this, blueprint); } protected void UpdateCollisionMaterials() { if (isLoaded) { int index = m_CollisionMaterial != null ? m_CollisionMaterial.handle.index : -1; for (int i = 0; i < solverIndices.count; i++) { if (solverIndices[i] < solver.collisionMaterials.count) solver.collisionMaterials[solverIndices[i]] = index; } } } public virtual void ProvideDeformableTriangles(ObiNativeIntList deformableTriangles, ObiNativeVector2List deformableUVs) { } public virtual void ProvideDeformableEdges(ObiNativeIntList deformableEdges) { } public virtual int GetDeformableEdgeCount() { return 0; } /// /// Copies all data (position, velocity, phase, etc) from one particle to another one. /// /// Index in the actor arrays of the particle we will copy data from. /// Index in the actor arrays of the particle we will copy data to. /// /// Whether the indices passed are within actor bounds. /// /// Extend this method to implement copying your own custom particle data in custom actors. public virtual bool CopyParticle(int actorSourceIndex, int actorDestIndex) { if (!isLoaded || actorSourceIndex < 0 || actorSourceIndex >= solverIndices.count || actorDestIndex < 0 || actorDestIndex >= solverIndices.count) return false; int sourceIndex = solverIndices[actorSourceIndex]; int destIndex = solverIndices[actorDestIndex]; // Copy solver data: m_Solver.prevPositions[destIndex] = m_Solver.prevPositions[sourceIndex]; m_Solver.restPositions[destIndex] = m_Solver.restPositions[sourceIndex]; m_Solver.endPositions[destIndex] = m_Solver.startPositions[destIndex] = m_Solver.positions[destIndex] = m_Solver.positions[sourceIndex]; m_Solver.prevOrientations[destIndex] = m_Solver.prevOrientations[sourceIndex]; m_Solver.restOrientations[destIndex] = m_Solver.restOrientations[sourceIndex]; m_Solver.endOrientations[destIndex] = m_Solver.startOrientations[destIndex] = m_Solver.orientations[destIndex] = m_Solver.orientations[sourceIndex]; m_Solver.velocities[destIndex] = m_Solver.velocities[sourceIndex]; m_Solver.angularVelocities[destIndex] = m_Solver.angularVelocities[sourceIndex]; m_Solver.invMasses[destIndex] = m_Solver.invMasses[sourceIndex]; m_Solver.invRotationalMasses[destIndex] = m_Solver.invRotationalMasses[sourceIndex]; m_Solver.principalRadii[destIndex] = m_Solver.principalRadii[sourceIndex]; m_Solver.phases[destIndex] = m_Solver.phases[sourceIndex]; m_Solver.filters[destIndex] = m_Solver.filters[sourceIndex]; m_Solver.colors[destIndex] = m_Solver.colors[sourceIndex]; return true; } /// /// Teleports one actor particle to a certain position in solver space. /// /// Index in the actor arrays of the particle we will teeleport. /// Position to teleport the particle to, expressed in solver space. public void TeleportParticle(int actorIndex, Vector3 position) { if (!isLoaded || actorIndex < 0 || actorIndex >= solverIndices.count) return; int solverIndex = solverIndices[actorIndex]; Vector4 delta = (Vector4)position - m_Solver.positions[solverIndex]; m_Solver.positions[solverIndex] += delta; m_Solver.prevPositions[solverIndex] += delta; m_Solver.endPositions[solverIndex] += delta; m_Solver.startPositions[solverIndex] += delta; } /// /// Teleports the entire actor to a new location / orientation. /// /// World space position to teleport the actor to. /// World space rotation to teleport the actor to. public virtual Matrix4x4 Teleport(Vector3 position, Quaternion rotation) { if (!isLoaded) return Matrix4x4.identity; // Subtract current transform position/rotation, then add new world space position/rotation. // Lastly, set the transform to the new position/rotation. Matrix4x4 offset = solver.transform.worldToLocalMatrix * Matrix4x4.TRS(position, Quaternion.identity, Vector3.one) * Matrix4x4.TRS(Vector3.zero, rotation, Vector3.one) * Matrix4x4.TRS(Vector3.zero, Quaternion.Inverse(transform.rotation), Vector3.one) * Matrix4x4.TRS(-transform.position, Quaternion.identity, Vector3.one) * solver.transform.localToWorldMatrix; Quaternion rotOffset = offset.rotation; for (int i = 0; i < solverIndices.count; i++) { int solverIndex = solverIndices[i]; m_Solver.positions[solverIndex] = m_Solver.prevPositions[solverIndex] = m_Solver.endPositions[solverIndex] = m_Solver.startPositions[solverIndex] = offset.MultiplyPoint3x4(m_Solver.positions[solverIndex]); m_Solver.orientations[solverIndex] = m_Solver.prevOrientations[solverIndex] = m_Solver.endOrientations[solverIndex] = m_Solver.startOrientations[solverIndex] = rotOffset * m_Solver.orientations[solverIndex]; m_Solver.velocities[solverIndex] = Vector4.zero; m_Solver.angularVelocities[solverIndex] = Vector4.zero; } transform.position = position; transform.rotation = rotation; return offset; } protected virtual void SwapWithFirstInactiveParticle(int actorIndex) { // update solver indices: m_Solver.particleToActor[solverIndices[actorIndex]].indexInActor = activeParticleCount; m_Solver.particleToActor[solverIndices[activeParticleCount]].indexInActor = actorIndex; solverIndices.Swap(actorIndex, activeParticleCount); } /// /// Activates one particle. /// /// /// True if a particle could be activated. False if there are no particles to activate. /// /// This operation preserves the relative order of all particles. public virtual bool ActivateParticle() { if (activeParticleCount >= particleCount) return false; // set active particle radius W to 1. var radii = m_Solver.principalRadii[solverIndices[activeParticleCount]]; radii.w = 1; m_Solver.principalRadii[solverIndices[activeParticleCount]] = radii; m_ActiveParticleCount[0]++; m_Solver.dirtyActiveParticles = true; m_Solver.dirtySimplices |= simplexTypes; return true; } /// /// Deactivates one particle. /// /// Index in the actor arrays of the particle we will deactivate. /// /// True if the particle was active. False if the particle was already inactive. /// /// This operation does not preserve the relative order of other particles, because the last active particle will /// swap positions with the particle being deactivated. public virtual bool DeactivateParticle(int actorIndex) { if (!IsParticleActive(actorIndex)) return false; m_ActiveParticleCount[0]--; // set inactive particle W to zero, this allows renderers to ignore it. var radii = m_Solver.principalRadii[solverIndices[actorIndex]]; radii.w = 0; m_Solver.principalRadii[solverIndices[actorIndex]] = radii; SwapWithFirstInactiveParticle(actorIndex); m_Solver.dirtyActiveParticles = true; m_Solver.dirtySimplices |= simplexTypes; return true; } /// /// Returns whether a given particle is active. /// /// Index in the actor arrays of the particle. /// /// True if the particle is active. False if the particle is inactive. /// public virtual bool IsParticleActive(int actorIndex) { return actorIndex < activeParticleCount; } /// /// Updates particle phases in the solver at runtime, including or removing the self-collision flag. /// public virtual void SetSelfCollisions(bool selfCollisions) { if (m_Solver != null && Application.isPlaying && isLoaded) { for (int i = 0; i < particleCount; i++) { if (selfCollisions) m_Solver.phases[solverIndices[i]] |= (int)ObiUtils.ParticleFlags.SelfCollide; else m_Solver.phases[solverIndices[i]] &= ~(int)ObiUtils.ParticleFlags.SelfCollide; } } } /// /// Updates particle phases in the solver at runtime, including or removing the self-collision flag. /// public virtual void SetDirectionalCollisions(bool dfCollisions) { if (m_Solver != null && Application.isPlaying && isLoaded) { for (int i = 0; i < particleCount; i++) { var norm = m_Solver.normals[solverIndices[i]]; if (dfCollisions) norm.w = sharedBlueprint.restNormals[i].w; else norm.w = 0; m_Solver.normals[solverIndices[i]] = norm; } } } /// /// Updates particle phases in the solver at runtime, including or removing the one-sided flag. /// public virtual void SetOneSided(bool oneSided) { if (m_Solver != null && Application.isPlaying && isLoaded) { for (int i = 0; i < particleCount; i++) { if (oneSided) m_Solver.phases[solverIndices[i]] |= (int)ObiUtils.ParticleFlags.OneSided; else m_Solver.phases[solverIndices[i]] &= ~(int)ObiUtils.ParticleFlags.OneSided; } } } /// /// Marks simplices dirty. /// public void SetSimplicesDirty() { if (m_Solver != null) m_Solver.dirtySimplices |= simplexTypes; } /// /// Marks a given constraint type as dirty. /// /// Type of the constraints that need re-creation. /// This will cause the solver to perform a constraint re-creation at the start of the next step. Needed when the constraint data in an actor changes at runtime, /// as a result of changing topology (torn cloth or ropes), or changes in internal constraint parameters such as compliance values. This is a relatively expensive operation, /// so it's best to amortize as many constraint modification operations as possible in a single step. public void SetConstraintsDirty(Oni.ConstraintType constraintType) { if (m_Solver != null) m_Solver.dirtyConstraints |= 1 << (int)constraintType; } /// /// Marks rendering dirty. /// public void SetRenderingDirty(Oni.RenderingSystemType rendererType) { if (m_Solver != null) m_Solver.dirtyRendering |= (int)rendererType; } /// /// Returns the data representation of constraints of a given type being simulated by this solver. /// /// Type of the constraints that will be returned by this method. /// /// The runtime constraints of the type speficied. Most constraints are stored in the blueprint, with a couple notable exceptions: pin and stitch constraints /// are always created at runtime, so they're not stored in the blueprint but in the actor itself. /// public IObiConstraints GetConstraintsByType(Oni.ConstraintType type) { // pin constraints are a special case, because they're not stored in a blueprint. They are created at runtime at stored in the actor itself. if (type == Oni.ConstraintType.Pin) return m_PinConstraints; if (type == Oni.ConstraintType.Pinhole) return m_PinholeConstraints; if (sharedBlueprint != null) return sharedBlueprint.GetConstraintsByType(type); return null; } /// /// Call when some particle properties have been modified and need updating. /// /// Does not do anything by default. Call when manually modifying particle properties in the solver, should the actor need to do some book keeping. /// For softbodies, updates their rest state. public virtual void UpdateParticleProperties() { } /// /// Returns the index of this particle in the solver arrays. /// /// Index of the particle in the actor arrays. /// /// The index of a given particle in the solver arrays. /// /// At runtime when the blueprint is loaded, this is the same as calling actor.solverIndices[solverIndex]. /// If the blueprint is not loaded it will return the same index passed to it: actorIndex. /// Note that this function does not perform any range checking. public int GetParticleRuntimeIndex(int actorIndex) { if (isLoaded) return solverIndices[actorIndex]; return actorIndex; } /// /// Given a solver particle index, returns the position of that particle in world space. /// /// Index of the particle in the solver arrays. /// /// The position of a given particle in world space. /// public Vector3 GetParticlePosition(int solverIndex) { if (isLoaded) return m_Solver.transform.TransformPoint(m_Solver.renderablePositions[solverIndex]); return Vector3.zero; } /// /// Given a solver particle index, returns the orientation of that particle in world space. /// /// Index of the particle in the solver arrays. /// /// The orientation of a given particle in world space. /// public Quaternion GetParticleOrientation(int solverIndex) { if (isLoaded) return m_Solver.transform.rotation * m_Solver.renderableOrientations[solverIndex]; return Quaternion.identity; } /// /// Given a solver particle index, returns the rest position of that particle. /// /// Index of the particle in the solver arrays. /// /// The position of a given particle in world space. /// public Vector3 GetParticleRestPosition(int solverIndex) { if (isLoaded) return m_Solver.restPositions[solverIndex]; return Vector3.zero; } /// /// Given a solver particle index, returns the rest orientation of that particle. /// /// Index of the particle in the solver arrays. /// /// The orientation of a given particle in world space. /// public Quaternion GetParticleRestOrientation(int solverIndex) { if (isLoaded) return m_Solver.restOrientations[solverIndex]; return Quaternion.identity; } /// /// Given a solver particle index, returns the anisotropic frame of that particle in world space. /// /// Index of the particle in the solver arrays. /// First basis vector of the frame. Contains particle radius along this axis in the 4th position. /// Second basis vector of the frame. Contains particle radius along this axis in the 4th position.. /// Third basis vector of the frame. Contains particle radius along this axis in the 4th position. public void GetParticleAnisotropy(int solverIndex, ref Vector4 b1, ref Vector4 b2, ref Vector4 b3) { if (isLoaded && usesAnisotropicParticles) { b1 = m_Solver.transform.TransformDirection(m_Solver.renderableOrientations[solverIndex] * Vector3.right); b2 = m_Solver.transform.TransformDirection(m_Solver.renderableOrientations[solverIndex] * Vector3.up); b3 = m_Solver.transform.TransformDirection(m_Solver.renderableOrientations[solverIndex] * Vector3.forward); b1[3] = m_Solver.maxScale * m_Solver.renderableRadii[solverIndex][0]; b2[3] = m_Solver.maxScale * m_Solver.renderableRadii[solverIndex][1]; b3[3] = m_Solver.maxScale * m_Solver.renderableRadii[solverIndex][2]; } else { b1[3] = b2[3] = b3[3] = m_Solver.maxScale * m_Solver.renderableRadii[solverIndex][0]; } } /// /// Given a solver particle index, returns the maximum world space radius of that particle, in any axis. /// /// Index of the particle in the solver arrays. /// /// The maximum radius of a given particle in world space. /// public float GetParticleMaxRadius(int solverIndex) { if (isLoaded) return m_Solver.maxScale * m_Solver.principalRadii[solverIndex][0]; return 0; } /// /// Given a solver particle index, returns the color of that particle. /// /// Index of the particle in the solver arrays. /// /// The color of the particle. /// public Color GetParticleColor(int solverIndex) { if (isLoaded) return m_Solver.colors[solverIndex]; return Color.white; } /// /// Sets a given category value for all particles in the actor. /// /// Category value. public void SetFilterCategory(int newCategory) { newCategory = Mathf.Clamp(newCategory, ObiUtils.MinCategory, ObiUtils.MaxCategory); for (int i = 0; i < particleCount; ++i) { int solverIndex = solverIndices[i]; var mask = ObiUtils.GetMaskFromFilter(solver.filters[solverIndex]); solver.filters[solverIndex] = ObiUtils.MakeFilter(mask, newCategory); } } /// /// Sets a given mask value for all particles in the actor. /// /// Mask value. public void SetFilterMask(int newMask) { newMask = Mathf.Clamp(newMask, ObiUtils.CollideWithNothing, ObiUtils.CollideWithEverything); for (int i = 0; i < particleCount; ++i) { int solverIndex = solverIndices[i]; var category = ObiUtils.GetCategoryFromFilter(solver.filters[solverIndex]); solver.filters[solverIndex] = ObiUtils.MakeFilter(newMask, category); } } /// /// Sets the inverse mass of each particle so that the total actor mass matches the one passed by parameter. /// /// The actor's total mass. public void SetMass(float mass) { if (Application.isPlaying && isLoaded && activeParticleCount > 0) { float invMass = 1.0f / (mass / activeParticleCount); for (int i = 0; i < activeParticleCount; ++i) { int solverIndex = solverIndices[i]; m_Solver.invMasses[solverIndex] = invMass; m_Solver.invRotationalMasses[solverIndex] = invMass; } UpdateParticleProperties(); } } /// /// Returns the actor's mass (sum of all particle masses), and the position of its center of mass. /// /// The actor's center of mass, expressed in solver space. /// Particles with infinite mass (invMass = 0) are ignored. public float GetMass(out Vector3 com) { float actorMass = 0; com = Vector3.zero; if (Application.isPlaying && isLoaded && activeParticleCount > 0) { Vector4 com4 = Vector4.zero; for (int i = 0; i < activeParticleCount; ++i) { if (m_Solver.invMasses[solverIndices[i]] > 0) { float mass = 1.0f / m_Solver.invMasses[solverIndices[i]]; actorMass += mass; com4 += m_Solver.positions[solverIndices[i]] * mass; } } com = com4; if (actorMass > float.Epsilon) com /= actorMass; } return actorMass; } /// ///Adds an external force to all particles in the actor. /// /// Value expressed in solver space. /// Type of "force" applied. public void AddForce(Vector3 force, ForceMode forceMode) { if (force.sqrMagnitude > Mathf.Epsilon) bufferedForces.dirty = true; switch (forceMode) { case ForceMode.Force: bufferedForces.force += (Vector4)force; break; case ForceMode.Acceleration: bufferedForces.acceleration += (Vector4)force; break; case ForceMode.Impulse: bufferedForces.impulse += (Vector4)force; break; case ForceMode.VelocityChange: bufferedForces.velChange += (Vector4)force; break; } } /// /// Adds a torque to the actor. /// /// Value expressed in solver space. /// Type of "torque" applied. public void AddTorque(Vector3 force, ForceMode forceMode) { if (force.sqrMagnitude > Mathf.Epsilon) bufferedForces.dirty = true; switch (forceMode) { case ForceMode.Force: bufferedForces.angularForce += (Vector4)force; break; case ForceMode.Acceleration: bufferedForces.angularAcceleration += (Vector4)force; break; case ForceMode.Impulse: bufferedForces.angularImpulse += (Vector4)force; break; case ForceMode.VelocityChange: bufferedForces.angularVelChange += (Vector4)force; break; } } #region Blueprints private void LoadBlueprintParticles(ObiActorBlueprint bp) { Matrix4x4 l2sTransform = actorLocalToSolverMatrix; Quaternion l2sRotation = l2sTransform.rotation; for (int i = 0; i < solverIndices.count; i++) { int k = solverIndices[i]; if (bp.positions != null && i < bp.positions.Length) { m_Solver.endPositions[k] = m_Solver.startPositions[k] = m_Solver.prevPositions[k] = m_Solver.positions[k] = l2sTransform.MultiplyPoint3x4(bp.positions[i]); m_Solver.renderablePositions[k] = l2sTransform.MultiplyPoint3x4(bp.positions[i]); } if (bp.orientations != null && i < bp.orientations.Length) { m_Solver.endOrientations[k] = m_Solver.startOrientations[k] = m_Solver.prevOrientations[k] = m_Solver.orientations[k] = l2sRotation * bp.orientations[i]; m_Solver.renderableOrientations[k] = l2sRotation * bp.orientations[i]; } // for softbodies, xyz values store SDF normal in particle's local space: needs to be transformed using particle orientation during simulation. // w value stores sparse SDF if < 0. if (bp.restNormals != null && i < bp.restNormals.Length) m_Solver.normals[k] = bp.restNormals[i]; if (bp.restPositions != null && i < bp.restPositions.Length) m_Solver.restPositions[k] = bp.restPositions[i]; if (bp.restOrientations != null && i < bp.restOrientations.Length) m_Solver.restOrientations[k] = bp.restOrientations[i]; if (bp.velocities != null && i < bp.velocities.Length) m_Solver.velocities[k] = l2sTransform.MultiplyVector(bp.velocities[i]); if (bp.angularVelocities != null && i < bp.angularVelocities.Length) m_Solver.angularVelocities[k] = l2sTransform.MultiplyVector(bp.angularVelocities[i]); if (bp.invMasses != null && i < bp.invMasses.Length) m_Solver.invMasses[k] = bp.invMasses[i] / m_MassScale; if (bp.invRotationalMasses != null && i < bp.invRotationalMasses.Length) m_Solver.invRotationalMasses[k] = bp.invRotationalMasses[i] / m_MassScale; if (bp.principalRadii != null && i < bp.principalRadii.Length) { Vector4 radii = bp.principalRadii[i]; radii.w = i < sourceBlueprint.activeParticleCount ? 1 : 0; m_Solver.principalRadii[k] = radii; } else { // need inactive emitter particles to zero as their flag. m_Solver.principalRadii[k] = Vector4.zero; } if (bp.filters != null && i < bp.filters.Length) m_Solver.filters[k] = bp.filters[i]; if (bp.colors != null && i < bp.colors.Length) m_Solver.colors[k] = bp.colors[i]; m_Solver.phases[k] = ObiUtils.MakePhase(groupID, 0); } m_ActiveParticleCount[0] = sourceBlueprint.activeParticleCount; m_Solver.dirtyActiveParticles = true; m_Solver.dirtyDeformableTriangles = true; m_Solver.dirtyDeformableEdges = true; m_Solver.dirtySimplices |= simplexTypes; m_Solver.dirtyConstraints |= ~0; // Push collision materials: UpdateCollisionMaterials(); } private void UnloadBlueprintParticles() { // Update active particles. m_ActiveParticleCount[0] = 0; m_Solver.dirtyActiveParticles = true; m_Solver.dirtyDeformableTriangles = true; m_Solver.dirtyDeformableEdges = true; m_Solver.dirtySimplices |= simplexTypes; m_Solver.dirtyConstraints |= ~0; } /// /// Resets the position and velocity of all particles, to the values stored in the blueprint. /// /// Note however /// that this does not affect constraints, so if you've torn a cloth/rope or resized a rope, calling ResetParticles won't restore /// the initial topology of the actor. public void ResetParticles() { if (isLoaded) { Matrix4x4 l2sTransform = actorLocalToSolverMatrix; Quaternion l2sRotation = l2sTransform.rotation; for (int i = 0; i < particleCount; ++i) { int solverIndex = solverIndices[i]; solver.startPositions[solverIndex] = solver.endPositions[solverIndex] = solver.positions[solverIndex] = l2sTransform.MultiplyPoint3x4(sourceBlueprint.positions[i]); solver.velocities[solverIndex] = l2sTransform.MultiplyVector(sourceBlueprint.velocities[i]); if (usesOrientedParticles) { solver.startOrientations[solverIndex] = solver.endOrientations[solverIndex] = solver.orientations[solverIndex] = l2sRotation * sourceBlueprint.orientations[i]; solver.angularVelocities[solverIndex] = l2sTransform.MultiplyVector(sourceBlueprint.angularVelocities[i]); } } } } #endregion #region State /// /// Resets the position and velocity of all particles, to the values stored in the blueprint. /// /// The blueprint that we want to fill with current particle data. /// Note that this will not resize the blueprint's data arrays, and that it does not perform range checking. For this reason, /// you must supply a blueprint large enough to store all particles' data. public bool SaveStateToBlueprint(ObiActorBlueprint bp) { if (bp == null || !m_Loaded) return false; Matrix4x4 l2sTransform = actorLocalToSolverMatrix.inverse; Quaternion l2sRotation = l2sTransform.rotation; // blueprint might have been regenerated, and reduced its size: for (int i = 0; i < solverIndices.count; i++) { int k = solverIndices[i]; if (bp.positions != null && m_Solver.positions != null && k < m_Solver.positions.count && i < bp.positions.Length) bp.positions[i] = l2sTransform.MultiplyPoint3x4(m_Solver.positions[k]); if (bp.velocities != null && m_Solver.velocities != null && k < m_Solver.velocities.count && i < bp.velocities.Length) bp.velocities[i] = l2sTransform.MultiplyVector(m_Solver.velocities[k]); } return true; } protected void StoreState() { DestroyImmediate(m_State); m_State = Instantiate(sourceBlueprint); SaveStateToBlueprint(m_State); } public void ClearState() { DestroyImmediate(m_State); } #endregion #region Solver callbacks /// /// Loads this actor's blueprint into the current solver. Automatically called by . /// internal virtual void LoadBlueprint() { var bp = sharedBlueprint; // in case we have temporary state, load that instead of the original blueprint. if (Application.isPlaying) { bp = m_State != null ? m_State : sourceBlueprint; } m_Loaded = true; LoadBlueprintParticles(bp); OnBlueprintLoaded?.Invoke(this, bp); } /// /// Unloads this actor's blueprint from a given solver. Automatically called by . /// internal virtual void UnloadBlueprint() { // instantiate blueprint and store current state in the instance: if (Application.isPlaying) StoreState(); m_Loaded = false; // unload the blueprint. UnloadBlueprintParticles(); OnBlueprintUnloaded?.Invoke(this, sharedBlueprint); } public virtual void SimulationStart(float timeToSimulate, float substepTime) { OnSimulationStart?.Invoke(this, timeToSimulate, substepTime); // Apply any buffered forces/torques: if (bufferedForces.dirty) { float mass = GetMass(out Vector3 com); if (!float.IsInfinity(mass)) { Vector4 accum; foreach (var p in solverIndices) { accum = bufferedForces.force / m_Solver.invMasses[p] / mass; accum += bufferedForces.acceleration / m_Solver.invMasses[p]; accum += bufferedForces.impulse / m_Solver.invMasses[p] / mass / timeToSimulate; accum += bufferedForces.velChange / m_Solver.invMasses[p] / timeToSimulate; m_Solver.externalForces[p] += accum; accum = bufferedForces.angularForce / m_Solver.invMasses[p] / mass; accum += bufferedForces.angularAcceleration / m_Solver.invMasses[p]; accum += bufferedForces.angularImpulse / m_Solver.invMasses[p] / mass / timeToSimulate; accum += bufferedForces.angularVelChange / m_Solver.invMasses[p] / timeToSimulate; m_Solver.externalForces[p] += (Vector4)Vector3.Cross(accum, (Vector3)m_Solver.positions[p] - com); } } bufferedForces.Clear(); } } public virtual void CollisionDetectionStart(float simulatedTime, float substepTime) { OnCollisionDetectionStart?.Invoke(this, simulatedTime, substepTime); } public virtual void SubstepsStart(float simulatedTime, float substepTime) { OnSubstepsStart?.Invoke(this, simulatedTime, substepTime); } public virtual void SimulationEnd(float simulatedTime, float substepTime) { OnSimulationEnd?.Invoke(this, simulatedTime, substepTime); } public virtual void RequestReadback() { OnRequestReadback?.Invoke(this); } public virtual void Interpolate(float simulatedTime, float substepTime) { // Update particle positions/orientations in the solver: if (!Application.isPlaying && isLoaded) { Matrix4x4 l2sTransform = actorLocalToSolverMatrix; Quaternion l2sRotation = l2sTransform.rotation; for (int i = 0; i < solverIndices.count; i++) { int k = solverIndices[i]; if (sourceBlueprint.positions != null && i < sourceBlueprint.positions.Length) { m_Solver.renderablePositions[k] = m_Solver.positions[k] = m_Solver.startPositions[k] = m_Solver.endPositions[k] = l2sTransform.MultiplyPoint3x4(sourceBlueprint.positions[i]); } if (sourceBlueprint.orientations != null && i < sourceBlueprint.orientations.Length) { m_Solver.renderableOrientations[k] = m_Solver.orientations[k] = m_Solver.startOrientations[k] = m_Solver.endOrientations[k] = l2sRotation * sourceBlueprint.orientations[i]; } } } OnInterpolate?.Invoke(this, simulatedTime, substepTime); } public virtual void OnSolverVisibilityChanged(bool visible) { } #endregion } }