public override IObiConstraintsBatch Clone() { var clone = new ObiPinConstraintsBatch(this); // careful here: since IntPtr is not serializable and the pinBodies array can be null, use offsets count instead. clone.pinBodies.ResizeUninitialized(offsets.count); clone.particleIndices.ResizeUninitialized(particleIndices.count); clone.offsets.ResizeUninitialized(offsets.count); clone.restDarbouxVectors.ResizeUninitialized(restDarbouxVectors.count); clone.stiffnesses.ResizeUninitialized(stiffnesses.count); clone.breakThresholds.ResizeUninitialized(breakThresholds.count); if (pinBodies != null) { clone.pinBodies.CopyFrom(pinBodies); } clone.particleIndices.CopyFrom(particleIndices); clone.offsets.CopyFrom(offsets); clone.restDarbouxVectors.CopyFrom(restDarbouxVectors); clone.stiffnesses.CopyFrom(stiffnesses); clone.breakThresholds.CopyFrom(breakThresholds); return(clone); }
private void DisableAttachment(AttachmentType type) { if (isBound) { switch (type) { case AttachmentType.Dynamic: if (pinBatch != null) { var pins = m_Actor.GetConstraintsByType(Oni.ConstraintType.Pin) as ObiConstraints <ObiPinConstraintsBatch>; if (pins != null) { pins.RemoveBatch(pinBatch); if (actor.isLoaded) { m_Actor.SetConstraintsDirty(Oni.ConstraintType.Pin); } } attachedCollider = null; pinBatch = null; attachedColliderHandleIndex = -1; } break; case AttachmentType.Static: var solver = m_Actor.solver; var blueprint = m_Actor.sourceBlueprint; for (int i = 0; i < m_SolverIndices.Length; ++i) { int solverIndex = m_SolverIndices[i]; if (solverIndex >= 0 && solverIndex < solver.invMasses.count) { solver.invMasses[solverIndex] = blueprint.invMasses[i]; } } if (m_Actor.usesOrientedParticles) { for (int i = 0; i < m_SolverIndices.Length; ++i) { int solverIndex = m_SolverIndices[i]; if (solverIndex >= 0 && solverIndex < solver.invRotationalMasses.count) { solver.invRotationalMasses[solverIndex] = blueprint.invRotationalMasses[i]; } } } m_Actor.UpdateParticleProperties(); break; } } }
private void Disable(AttachmentType type) { var solver = m_Actor.solver; var blueprint = m_Actor.blueprint; if (isBound && blueprint != null && solver != null) { switch (type) { case AttachmentType.Dynamic: var pins = m_Actor.GetConstraintsByType(Oni.ConstraintType.Pin) as ObiConstraints <ObiPinConstraintsBatch>; if (pins != null && pinBatch != null) { pinBatch.SetEnabled(false); pinBatch.RemoveFromSolver(); pins.RemoveBatch(pinBatch); pinBatch = null; } break; case AttachmentType.Static: for (int i = 0; i < m_SolverIndices.Length; ++i) { if (m_SolverIndices[i] >= 0 && m_SolverIndices[i] < solver.invMasses.count) { solver.invMasses[m_SolverIndices[i]] = blueprint.invMasses[i]; } } if (m_Actor.usesOrientedParticles) { for (int i = 0; i < m_SolverIndices.Length; ++i) { if (m_SolverIndices[i] >= 0 && m_SolverIndices[i] < solver.invRotationalMasses.count) { solver.invRotationalMasses[m_SolverIndices[i]] = blueprint.invRotationalMasses[i]; } } } m_Actor.UpdateParticleProperties(); break; } } }
public override IObiConstraintsBatch Clone(IObiConstraints constraints) { var clone = new ObiPinConstraintsBatch(constraints as ObiPinConstraintsData, this); // careful here: since IntPtr is not serializable and the pinBodies array can be null, use offsets count instead. clone.pinBodies.Capacity = offsets.count; clone.pinBodies.Clear(); if (pinBodies != null) { for (int i = 0; i < offsets.count; ++i) { clone.pinBodies.Add(pinBodies[i]); } } else { for (int i = 0; i < offsets.count; ++i) { clone.pinBodies.Add(new ObiColliderHandle()); } } clone.particleIndices.ResizeUninitialized(particleIndices.count); clone.offsets.ResizeUninitialized(offsets.count); clone.restDarbouxVectors.ResizeUninitialized(restDarbouxVectors.count); clone.stiffnesses.ResizeUninitialized(stiffnesses.count); clone.breakThresholds.ResizeUninitialized(breakThresholds.count); clone.particleIndices.CopyFrom(particleIndices); clone.offsets.CopyFrom(offsets); clone.restDarbouxVectors.CopyFrom(restDarbouxVectors); clone.stiffnesses.CopyFrom(stiffnesses); clone.breakThresholds.CopyFrom(breakThresholds); return(clone); }
public ObiPinConstraintsBatch(ObiPinConstraintsData constraints = null, ObiPinConstraintsBatch source = null) : base(source) { m_Constraints = constraints; }
protected override IEnumerator Initialize() { if (inputMesh == null || !inputMesh.isReadable) { // TODO: return an error in the coroutine. Debug.LogError("The input mesh is null, or not readable."); yield break; } ClearParticleGroups(); Vector3[] vertices = inputMesh.vertices; Vector3[] normals = inputMesh.normals; vertexToParticle = new int[vertices.Length]; List <Vector3> particles = new List <Vector3>(); // Add particles to every vertex, as long as they are not too close to the already added ones: for (int i = 0; i < vertices.Length; ++i) { bool intersects = false; Vector3 vertexScaled = Vector3.Scale(scale, vertices[i]); for (int j = 0; j < particles.Count; ++j) { if (Vector3.Distance(vertexScaled, particles[j]) < particleRadius * 2 * (1 - particleOverlap)) { intersects = true; break; } } if (intersects) { continue; } particles.Add(vertexScaled); if (i % 100 == 0) { yield return(new CoroutineJob.ProgressInfo("ObiSoftbody: sampling mesh...", i / (float)vertices.Length)); } } // Find out the closes particle to each vertex: for (int i = 0; i < vertices.Length; ++i) { Vector3 vertexScaled = Vector3.Scale(scale, vertices[i]); float minDistance = float.MaxValue; vertexToParticle[i] = 0; for (int j = 0; j < particles.Count; ++j) { float distance = Vector3.SqrMagnitude(vertexScaled - particles[j]); if (distance < minDistance) { minDistance = distance; vertexToParticle[i] = j; } } if (i % 100 == 0) { yield return(new CoroutineJob.ProgressInfo("ObiSoftbody: mapping vertices to particles...", i / (float)vertices.Length)); } } positions = new Vector3[particles.Count]; orientations = new Quaternion[particles.Count]; restPositions = new Vector4[particles.Count]; restOrientations = new Quaternion[particles.Count]; velocities = new Vector3[particles.Count]; angularVelocities = new Vector3[particles.Count]; invMasses = new float[particles.Count]; invRotationalMasses = new float[particles.Count]; principalRadii = new Vector3[particles.Count]; phases = new int[particles.Count]; colors = new Color[particles.Count]; m_ActiveParticleCount = particles.Count; for (int i = 0; i < particles.Count; ++i) { // Perform ellipsoid fitting: Vector3 avgNormal = Vector3.zero; List <Vector3> neighbourVertices = new List <Vector3>(); for (int j = 0; j < vertices.Length; ++j) { Vector3 vertexScaled = Vector3.Scale(scale, vertices[j]); if (Vector3.Distance(vertexScaled, particles[i]) < anisotropyNeighborhood) { neighbourVertices.Add(vertexScaled); avgNormal += normals[j]; } } if (neighbourVertices.Count > 0) { avgNormal /= neighbourVertices.Count; } Vector3 centroid = particles[i]; Quaternion orientation = Quaternion.identity; Vector3 principalValues = Vector3.one; Oni.GetPointCloudAnisotropy(neighbourVertices.ToArray(), neighbourVertices.Count, maxAnisotropy, particleRadius, ref avgNormal, ref centroid, ref orientation, ref principalValues); invRotationalMasses[i] = invMasses[i] = 1.0f; positions[i] = Vector3.Lerp(particles[i], centroid, shapeSmoothing); restPositions[i] = positions[i]; restPositions[i][3] = 1; // activate rest position. orientations[i] = orientation; restOrientations[i] = orientation; principalRadii[i] = principalValues; phases[i] = Oni.MakePhase(1, oneSided ? Oni.ParticleFlags.OneSided : 0); colors[i] = Color.white; if (i % 100 == 0) { yield return(new CoroutineJob.ProgressInfo("ObiSoftbody: generating particles...", i / (float)particles.Count)); } } //if (makeSolidCluster){ /*indices.Clear(); * for (int i = 0; i < particles.Count; ++i) * indices.Add(i); * shapeBatch.AddConstraint(indices.ToArray(),1,0,0,true); * }*/ IEnumerator sc = CreateShapeMatchingConstraints(particles); while (sc.MoveNext()) { yield return(sc.Current); } // Initialize pin constraints: pinConstraintsData = new ObiPinConstraintsData(); ObiPinConstraintsBatch pinBatch = new ObiPinConstraintsBatch(); pinConstraintsData.AddBatch(pinBatch); generatedMesh = inputMesh; }
private void EnableAttachment(AttachmentType type) { if (enabled && m_Actor.isLoaded && isBound) { var solver = m_Actor.solver; switch (type) { case AttachmentType.Dynamic: var pins = m_Actor.GetConstraintsByType(Oni.ConstraintType.Pin) as ObiPinConstraintsData; attachedCollider = m_Target.GetComponent <ObiColliderBase>(); if (pins != null && attachedCollider != null && pinBatch == null) { // create a new data batch with all our pin constraints: pinBatch = new ObiPinConstraintsBatch(pins); for (int i = 0; i < m_SolverIndices.Length; ++i) { pinBatch.AddConstraint(m_SolverIndices[i], attachedCollider, m_PositionOffsets[i], m_OrientationOffsets[i], m_Compliance, constrainOrientation ? 0 : 10000, m_BreakThreshold); pinBatch.activeConstraintCount++; } // add the batch to the actor: pins.AddBatch(pinBatch); // store the attached collider's handle: attachedColliderHandleIndex = -1; if (attachedCollider.Handle != null) { attachedColliderHandleIndex = attachedCollider.Handle.index; } m_Actor.SetConstraintsDirty(Oni.ConstraintType.Pin); } break; case AttachmentType.Static: for (int i = 0; i < m_SolverIndices.Length; ++i) { if (m_SolverIndices[i] >= 0 && m_SolverIndices[i] < solver.invMasses.count) { solver.invMasses[m_SolverIndices[i]] = 0; } } if (m_Actor.usesOrientedParticles && m_ConstrainOrientation) { for (int i = 0; i < m_SolverIndices.Length; ++i) { if (m_SolverIndices[i] >= 0 && m_SolverIndices[i] < solver.invRotationalMasses.count) { solver.invRotationalMasses[m_SolverIndices[i]] = 0; } } } m_Actor.UpdateParticleProperties(); break; } } }
protected override IEnumerator Initialize() { if (inputMesh == null || !inputMesh.isReadable) { // TODO: return an error in the coroutine. Debug.LogError("The input mesh is null, or not readable."); yield break; } ClearParticleGroups(); List <Vector3> particles = new List <Vector3>(); List <Vector3> normals = new List <Vector3>(); // Calculate voxel size so that no more than 32^3 particles are created: Vector3 boundsSize = Vector3.Scale(inputMesh.bounds.size, Vector3.one); float voxelSize = Mathf.Max(boundsSize.x / 32.0f, boundsSize.y / 32.0f, boundsSize.z / 32.0f, particleRadius * 2 * (1 - particleOverlap)); // Voxelize mesh and calculate discrete distance field: MeshVoxelizer voxelizer = new MeshVoxelizer(inputMesh, voxelSize); VoxelDistanceField df = new VoxelDistanceField(voxelizer); voxelizer.Voxelize(scale); df.JumpFlood(); MeshVoxelizer.Voxel[,,] voxels = voxelizer.voxels; for (int x = 0; x < voxels.GetLength(0); ++x) { for (int y = 0; y < voxels.GetLength(1); ++y) { for (int z = 0; z < voxels.GetLength(2); ++z) { if (voxels[x, y, z] != MeshVoxelizer.Voxel.Outside) { particles.Add(new Vector3(voxelizer.Origin.x + x + 0.5f, voxelizer.Origin.y + y + 0.5f, voxelizer.Origin.z + z + 0.5f) * voxelSize); normals.Add(df.distanceField[x, y, z] - new Vector3Int(x, y, z)); } } } } positions = new Vector3[particles.Count]; orientations = new Quaternion[particles.Count]; restPositions = new Vector4[particles.Count]; restOrientations = new Quaternion[particles.Count]; velocities = new Vector3[particles.Count]; angularVelocities = new Vector3[particles.Count]; invMasses = new float[particles.Count]; invRotationalMasses = new float[particles.Count]; principalRadii = new Vector3[particles.Count]; phases = new int[particles.Count]; colors = new Color[particles.Count]; m_ActiveParticleCount = particles.Count; for (int i = 0; i < particles.Count; ++i) { // Perform ellipsoid fitting: Vector3 avgNormal = Vector3.zero; List <Vector3> neighbourVertices = new List <Vector3>(); Vector3 centroid = particles[i]; Quaternion orientation = Quaternion.LookRotation(normals[i]); Vector3 principalValues = Vector3.one * voxelSize * (0.5f + particleOverlap); invRotationalMasses[i] = invMasses[i] = 1.0f; positions[i] = Vector3.Lerp(particles[i], centroid, shapeSmoothing); restPositions[i] = positions[i]; orientations[i] = orientation; restOrientations[i] = orientation; restPositions[i][3] = 1; // activate rest position. principalRadii[i] = principalValues; phases[i] = ObiUtils.MakePhase(1, 0); //Oni.MakePhase(1, (selfCollisions ? Oni.ParticlePhase.SelfCollide : 0) | (oneSided ? Oni.ParticlePhase.OneSided : 0)); colors[i] = Color.white; if (i % 100 == 0) { yield return(new CoroutineJob.ProgressInfo("ObiSoftbody: generating particles...", i / (float)particles.Count)); } } IEnumerator sc = CreateShapeMatchingConstraints(particles); while (sc.MoveNext()) { yield return(sc.Current); } // Initialize pin constraints: pinConstraintsData = new ObiPinConstraintsData(); ObiPinConstraintsBatch pinBatch = new ObiPinConstraintsBatch(); pinConstraintsData.AddBatch(pinBatch); }
protected override IEnumerator Initialize() { if (inputMesh == null || !inputMesh.isReadable) { // TODO: return an error in the coroutine. Debug.LogError("The input mesh is null, or not readable."); yield break; } ClearParticleGroups(); topology = new HalfEdgeMesh(); topology.inputMesh = inputMesh; topology.Generate(); positions = new Vector3[topology.vertices.Count]; restPositions = new Vector4[topology.vertices.Count]; velocities = new Vector3[topology.vertices.Count]; invMasses = new float[topology.vertices.Count]; principalRadii = new Vector3[topology.vertices.Count]; phases = new int[topology.vertices.Count]; colors = new Color[topology.vertices.Count]; areaContribution = new float[topology.vertices.Count]; // Create a particle for each vertex: m_ActiveParticleCount = topology.vertices.Count; for (int i = 0; i < topology.vertices.Count; i++) { HalfEdgeMesh.Vertex vertex = topology.vertices[i]; // Get the particle's area contribution. areaContribution[i] = 0; foreach (HalfEdgeMesh.Face face in topology.GetNeighbourFacesEnumerator(vertex)) { areaContribution[i] += topology.GetFaceArea(face) / 3; } // Get the shortest neighbour edge, particle radius will be half of its length. float minEdgeLength = Single.MaxValue; foreach (HalfEdgeMesh.HalfEdge edge in topology.GetNeighbourEdgesEnumerator(vertex)) { // vertices at each end of the edge: Vector3 v1 = Vector3.Scale(scale, topology.vertices[topology.GetHalfEdgeStartVertex(edge)].position); Vector3 v2 = Vector3.Scale(scale, topology.vertices[edge.endVertex].position); minEdgeLength = Mathf.Min(minEdgeLength, Vector3.Distance(v1, v2)); } invMasses[i] = (/*skinnedMeshRenderer == null &&*/ areaContribution[i] > 0) ? (1.0f / (DEFAULT_PARTICLE_MASS * areaContribution[i])) : 0; positions[i] = Vector3.Scale(scale, vertex.position); restPositions[i] = positions[i]; restPositions[i][3] = 1; // activate rest position. principalRadii[i] = Vector3.one * minEdgeLength * 0.5f; phases[i] = Oni.MakePhase(1, /*selfCollisions ? Oni.ParticlePhase.SelfCollide : 0*/ 0); colors[i] = Color.white; if (i % 500 == 0) { yield return(new CoroutineJob.ProgressInfo("ObiCloth: generating particles...", i / (float)topology.vertices.Count)); } } IEnumerator dt = GenerateDeformableTriangles(); while (dt.MoveNext()) { yield return(dt.Current); } //Create distance constraints: IEnumerator dc = CreateDistanceConstraints(); while (dc.MoveNext()) { yield return(dc.Current); } // Create aerodynamic constraints: IEnumerator ac = CreateAerodynamicConstraints(); while (ac.MoveNext()) { yield return(ac.Current); } //Create bending constraints: IEnumerator bc = CreateBendingConstraints(); while (bc.MoveNext()) { yield return(bc.Current); } // Create skin constraints: IEnumerator sc = CreateSkinConstraints(); while (sc.MoveNext()) { yield return(sc.Current); } pinConstraintsData = new ObiPinConstraintsData(); ObiPinConstraintsBatch pinBatch = new ObiPinConstraintsBatch(); pinConstraintsData.AddBatch(pinBatch); }
private void Enable(AttachmentType type) { var solver = m_Actor.solver; var blueprint = m_Actor.blueprint; if (isBound && blueprint != null && m_Actor.solver != null) { switch (type) { case AttachmentType.Dynamic: var pins = m_Actor.GetConstraintsByType(Oni.ConstraintType.Pin) as ObiConstraints <ObiPinConstraintsBatch>; ObiColliderBase attachedCollider = m_Target.GetComponent <ObiColliderBase>(); if (pins != null && attachedCollider != null) { // create a new data batch with all our pin constraints: pinBatch = new ObiPinConstraintsBatch(); for (int i = 0; i < m_PositionOffsets.Length; ++i) { pinBatch.AddConstraint(0, attachedCollider, m_PositionOffsets[i], m_OrientationOffsets[i]); pinBatch.activeConstraintCount++; } // add the batch to the solver: pins.AddBatch(pinBatch); pinBatch.AddToSolver(pins); // override the pin indices with the ones we got at bind time: for (int i = 0; i < m_SolverIndices.Length; ++i) { pinBatch.particleIndices[i] = m_SolverIndices[i]; pinBatch.stiffnesses[i * 2] = m_Compliance; pinBatch.stiffnesses[i * 2 + 1] = constrainOrientation?0:10000; pinBatch.breakThresholds[i] = m_BreakThreshold; } // enable the batch: pinBatch.SetEnabled(true); } break; case AttachmentType.Static: for (int i = 0; i < m_SolverIndices.Length; ++i) { solver.invMasses[m_SolverIndices[i]] = 0; } if (m_Actor.usesOrientedParticles && m_ConstrainOrientation) { for (int i = 0; i < m_SolverIndices.Length; ++i) { solver.invRotationalMasses[m_SolverIndices[i]] = 0; } } m_Actor.UpdateParticleProperties(); break; } } }
public ObiPinConstraintsBatch(ObiPinConstraintsBatch source = null) : base(source) { }
protected override IEnumerator Initialize() { path.OnPathChanged.RemoveAllListeners(); path.OnControlPointAdded.RemoveAllListeners(); path.OnControlPointRemoved.RemoveAllListeners(); path.OnControlPointRenamed.RemoveAllListeners(); path.OnPathChanged.AddListener(GenerateImmediate); path.OnControlPointAdded.AddListener(ControlPointAdded); path.OnControlPointRemoved.AddListener(ControlPointRemoved); path.OnControlPointRenamed.AddListener(ControlPointRenamed); if (path.ControlPointCount < 2) { ClearParticleGroups(); path.InsertControlPoint(0, Vector3.left, Vector3.left * 0.25f, Vector3.right * 0.25f, Vector3.up, DEFAULT_PARTICLE_MASS, 1, 1, 1, Color.white, "control point"); path.InsertControlPoint(1, Vector3.right, Vector3.left * 0.25f, Vector3.right * 0.25f, Vector3.up, DEFAULT_PARTICLE_MASS, 1, 1, 1, Color.white, "control point"); } path.RecalculateLenght(Matrix4x4.identity, 0.00001f, 7); List <Vector3> particlePositions = new List <Vector3>(); List <float> particleThicknesses = new List <float>(); List <float> particleInvMasses = new List <float>(); List <int> particlePhases = new List <int>(); List <Color> particleColors = new List <Color>(); // In case the path is open, add a first particle. In closed paths, the last particle is also the first one. if (!path.Closed) { particlePositions.Add(path.points.GetPositionAtMu(path.Closed, 0)); particleThicknesses.Add(path.thicknesses.GetAtMu(path.Closed, 0)); particleInvMasses.Add(ObiUtils.MassToInvMass(path.thicknesses.GetAtMu(path.Closed, 0))); particlePhases.Add(path.phases.GetAtMu(path.Closed, 0)); particleColors.Add(path.colors.GetAtMu(path.Closed, 0)); } // Create a particle group for the first control point: groups[0].particleIndices.Clear(); groups[0].particleIndices.Add(0); ReadOnlyCollection <float> lengthTable = path.ArcLengthTable; int spans = path.GetSpanCount(); for (int i = 0; i < spans; i++) { int firstArcLengthSample = i * (path.ArcLengthSamples + 1); int lastArcLengthSample = (i + 1) * (path.ArcLengthSamples + 1); float upToSpanLength = lengthTable[firstArcLengthSample]; float spanLength = lengthTable[lastArcLengthSample] - upToSpanLength; int particlesInSpan = 1 + Mathf.FloorToInt(spanLength / thickness * resolution); float distance = spanLength / particlesInSpan; for (int j = 0; j < particlesInSpan; ++j) { float mu = path.GetMuAtLenght(upToSpanLength + distance * (j + 1)); particlePositions.Add(path.points.GetPositionAtMu(path.Closed, mu)); particleThicknesses.Add(path.thicknesses.GetAtMu(path.Closed, mu)); particleInvMasses.Add(ObiUtils.MassToInvMass(path.masses.GetAtMu(path.Closed, mu))); particlePhases.Add(path.phases.GetAtMu(path.Closed, mu)); particleColors.Add(path.colors.GetAtMu(path.Closed, mu)); } // Create a particle group for each control point: if (!(path.Closed && i == spans - 1)) { groups[i + 1].particleIndices.Clear(); groups[i + 1].particleIndices.Add(particlePositions.Count - 1); } if (i % 100 == 0) { yield return(new CoroutineJob.ProgressInfo("ObiRope: generating particles...", i / (float)spans)); } } m_ActiveParticleCount = particlePositions.Count; totalParticles = m_ActiveParticleCount + pooledParticles; int numSegments = m_ActiveParticleCount - (path.Closed ? 0 : 1); if (numSegments > 0) { m_InterParticleDistance = path.Length / (float)numSegments; } else { m_InterParticleDistance = 0; } positions = new Vector3[totalParticles]; restPositions = new Vector4[totalParticles]; velocities = new Vector3[totalParticles]; invMasses = new float[totalParticles]; principalRadii = new Vector3[totalParticles]; phases = new int[totalParticles]; colors = new Color[totalParticles]; restLengths = new float[totalParticles]; for (int i = 0; i < m_ActiveParticleCount; i++) { invMasses[i] = particleInvMasses[i]; positions[i] = particlePositions[i]; restPositions[i] = positions[i]; restPositions[i][3] = 1; // activate rest position. principalRadii[i] = Vector3.one * particleThicknesses[i] * thickness; phases[i] = Oni.MakePhase(particlePhases[i], 0); colors[i] = particleColors[i]; if (i % 100 == 0) { yield return(new CoroutineJob.ProgressInfo("ObiRope: generating particles...", i / (float)m_ActiveParticleCount)); } } //Create distance constraints for the total number of particles, but only activate for the used ones. IEnumerator dc = CreateDistanceConstraints(); while (dc.MoveNext()) { yield return(dc.Current); } //Create bending constraints: IEnumerator bc = CreateBendingConstraints(); while (bc.MoveNext()) { yield return(bc.Current); } //Create pin constraints: pinConstraintsData = new ObiPinConstraintsData(); ObiPinConstraintsBatch pinBatch = new ObiPinConstraintsBatch(); pinConstraintsData.AddBatch(pinBatch); // Recalculate rest length: m_RestLength = 0; foreach (float length in restLengths) { m_RestLength += length; } }