void dumpToMesh (OrientedBox &box, Mesh3D &mesh) { mesh.setColor(RGBColor::Cyan()); for (int i = 0; i < 200; i++) for (int j = 0; j < 200; j++) for (int k = 0; k < 200; k++) { Vector3dd T = Vector3dd(i - 100, j - 100, k - 100) / 10.0; T += box.position; if (box.isInside(T)) { mesh.addPoint(T); } } mesh.setColor(RGBColor::Yellow()); for (Vector3dd &p : box.getPoints()) { mesh.addPoint(p); } mesh.setColor(RGBColor::Green()); mesh.addOOB(box, true); }
bool TestYLineOBox ( Vector const & V, OrientedBox const & B ) { Vector point = B.transformToLocal(V); Vector normal = B.rotateToLocal(Vector::unitY); return TestLineABox( Line3d(point,normal), B.getLocalShape() ); }
ErrorCode leaf( EntityHandle node ) { OrientedBox box; ErrorCode rval = mTool->box( node, box ); if (MB_SUCCESS !=rval) return rval; EntityHandle h; rval = box.make_hex( h, mOut ); if (MB_SUCCESS !=rval) return rval; int i = hash_handle( node ); return mOut->tag_set_data( mTag, &h, 1, &i ); }
Sphere EncloseSphere ( OrientedBox const & shape ) { float x = shape.getExtentX(); float y = shape.getExtentY(); float z = shape.getExtentZ(); float radius = sqrt( x*x + y*y + z*z ); return Sphere( shape.getCenter(), radius ); }
Vector ClosestPointOBox ( Vector const & V, OrientedBox const & B ) { Vector lV = B.transformToLocal(V); AxialBox lB = B.getLocalShape(); Vector lC = ClosestPointABox(lV,lB); Vector out = B.transformToWorld(lC); return out; }
CkReductionMsg* boxGrowth(int nMsg, CkReductionMsg** msgs) { OrientedBox<T>* pbox = static_cast<OrientedBox<T> *>(msgs[0]->getData()); OrientedBox<T>* msgpbox; for(int i = 1; i < nMsg; i++) { msgpbox = static_cast<OrientedBox<T> *>(msgs[i]->getData()); if(msgpbox->initialized()) { pbox->grow(msgpbox->lesser_corner); pbox->grow(msgpbox->greater_corner); } } return CkReductionMsg::buildNew(sizeof(OrientedBox<T>), pbox); }
// a visit to a node, will create an OrientedBox object for that node, get a hex from that box and tag it with // an integer tag representing it's depth in the tree, and add it to the list of *hexes* for this tree ErrorCode OBBHexWriter::visit( EntityHandle node, int depth, bool& descend) { ErrorCode rval; //create a new tag for the tree depth of the obb std::string depth_tag_name = "TREE_DEPTH"; rval = mbi2->tag_get_handle( depth_tag_name.c_str(), 1, MB_TYPE_INTEGER, depth_tag, MB_TAG_DENSE|MB_TAG_CREAT); assert( MB_SUCCESS == rval ); if( MB_SUCCESS != rval ) return rval; OrientedBox box; rval = tool->box( node, box ); assert(MB_SUCCESS == rval ); if ( MB_SUCCESS != rval ) return rval; rval = box.make_hex( new_hex, mbi2 ); assert( MB_SUCCESS == rval ); if( MB_SUCCESS != rval ) return rval; void *ptr = &depth; rval = mbi2-> tag_set_data( depth_tag, &new_hex, 1, ptr); assert( MB_SUCCESS == rval ); if( MB_SUCCESS != rval ) return rval; if(w_tris) { //get the triangles contained by this node std::vector<EntityHandle> tris; rval = tool->get_moab_instance()->get_entities_by_type( node, MBTRI, tris); assert(MB_SUCCESS == rval ); if ( MB_SUCCESS != rval ) return rval; rval = transfer_tri_inst( tool->get_moab_instance(), mbi2, tris); assert( MB_SUCCESS == rval ); //add the data to the class attributes tri_map[new_hex] = tris; } //add the hex and go to the next tree depth hexes.push_back(new_hex); descend = true; return MB_SUCCESS; }
// Compute the granular "stress" Matrix3 PeriodicBCComputeStressStrain::calcGranularStress(const DEMParticlePArray& particles, const DEMContactArray& contacts, const OrientedBox& spatialDomain) { Matrix3 stress(0.0); // Basic component (Love-Weber) for (const auto& contact : contacts) { auto p1Center = contact.getP1()->currentPosition(); auto p2Center = contact.getP2()->currentPosition(); auto direction = -(p1Center - p2Center); auto force = contact.getNormalForce() + contact.getTangentForce(); stress += Dyad(force, direction); } // Component needed if # of particles in RVE is small // **TODO** // Inertial component (Nicot, 2013) Matrix3 inertial(0.0); for (const auto& particle : particles) { Vec momentJ = particle->getMomentJ(); Matrix3 localInertialTensor(momentJ.x(), 0.0, 0.0, 0.0, momentJ.y(), 0.0, 0.0, 0.0, momentJ.z()); Vec ax_a = particle->currentAxisA(); Vec ax_b = particle->currentAxisB(); Vec ax_c = particle->currentAxisC(); Matrix3 Q(ax_a.x(), ax_b.x(), ax_c.x(), ax_a.y(), ax_b.y(), ax_c.y(), ax_a.z(), ax_b.z(), ax_c.z()); Matrix3 chi = Q.Transpose() * localInertialTensor * Q; Vec omega = particle->currentAngularVelocity(); Vec localOmegaDot = particle->angularAcceleration(); Vec omegaDot = Q.Transpose() * localOmegaDot; inertial -= (chi * dot(omega, omega)); inertial += (Dyad(omega, omega) * chi); Matrix3 permu(0.0); for (int j = 0; j < 3; ++j) { permu(0,j) = omegaDot[1] * chi(j,2) - omega[2] * chi(j,1); permu(1,j) = omegaDot[2] * chi(j,0) - omega[0] * chi(j,2); permu(2,j) = omegaDot[0] * chi(j,1) - omega[1] * chi(j,0); } inertial += permu; } stress -= inertial; stress /= spatialDomain.volume(); return stress; }
Range ProjectAxis ( Line3d const & L, OrientedBox const & B ) { Vector const & N = L.getNormal(); real x = std::abs( Collision3d::ComponentAlong( B.getAxisX(), N ) ) * B.getExtentX(); real y = std::abs( Collision3d::ComponentAlong( B.getAxisY(), N ) ) * B.getExtentY(); real z = std::abs( Collision3d::ComponentAlong( B.getAxisZ(), N ) ) * B.getExtentZ(); real d = x + y + z; real c = ProjectAxis( L, B.getCenter() ); return Range( c - d, c + d ); }
bool TestOBoxOBox ( OrientedBox const & boxA, OrientedBox const & boxB ) { // convenience variables const Vector* akA = boxA.getAxes(); const Vector* akB = boxB.getAxes(); const float* afEA = boxA.getExtents(); const float* afEB = boxB.getExtents(); // compute difference of box centers, D = C1-C0 Vector kD = boxB.getCenter() - boxA.getCenter(); float aafC[3][3]; // matrix C = A^T B, c_{ij} = Dot(A_i,B_j) float aafAbsC[3][3]; // |c_{ij}| float afAD[3]; // Dot(A_i,D) float fR0, fR1, fR; // interval radii and distance between centers float fR01; // = R0 + R1 // axis C0+t*A0 aafC[0][0] = akA[0].dot(akB[0]); aafC[0][1] = akA[0].dot(akB[1]); aafC[0][2] = akA[0].dot(akB[2]); afAD[0] = akA[0].dot(kD); aafAbsC[0][0] = static_cast<float>(fabs(aafC[0][0])); aafAbsC[0][1] = static_cast<float>(fabs(aafC[0][1])); aafAbsC[0][2] = static_cast<float>(fabs(aafC[0][2])); fR = static_cast<float>(fabs(afAD[0])); fR1 = afEB[0]*aafAbsC[0][0]+afEB[1]*aafAbsC[0][1]+afEB[2]*aafAbsC[0][2]; fR01 = afEA[0] + fR1; if ( fR > fR01 ) return false; // axis C0+t*A1 aafC[1][0] = akA[1].dot(akB[0]); aafC[1][1] = akA[1].dot(akB[1]); aafC[1][2] = akA[1].dot(akB[2]); afAD[1] = akA[1].dot(kD); aafAbsC[1][0] = static_cast<float>(fabs(aafC[1][0])); aafAbsC[1][1] = static_cast<float>(fabs(aafC[1][1])); aafAbsC[1][2] = static_cast<float>(fabs(aafC[1][2])); fR = static_cast<float>(fabs(afAD[1])); fR1 = afEB[0]*aafAbsC[1][0]+afEB[1]*aafAbsC[1][1]+afEB[2]*aafAbsC[1][2]; fR01 = afEA[1] + fR1; if ( fR > fR01 ) return false; // axis C0+t*A2 aafC[2][0] = akA[2].dot(akB[0]); aafC[2][1] = akA[2].dot(akB[1]); aafC[2][2] = akA[2].dot(akB[2]); afAD[2] = akA[2].dot(kD); aafAbsC[2][0] = static_cast<float>(fabs(aafC[2][0])); aafAbsC[2][1] = static_cast<float>(fabs(aafC[2][1])); aafAbsC[2][2] = static_cast<float>(fabs(aafC[2][2])); fR = static_cast<float>(fabs(afAD[2])); fR1 = afEB[0]*aafAbsC[2][0]+afEB[1]*aafAbsC[2][1]+afEB[2]*aafAbsC[2][2]; fR01 = afEA[2] + fR1; if ( fR > fR01 ) return false; // axis C0+t*B0 fR = static_cast<float>(fabs(akB[0].dot(kD))); fR0 = afEA[0]*aafAbsC[0][0]+afEA[1]*aafAbsC[1][0]+afEA[2]*aafAbsC[2][0]; fR01 = fR0 + afEB[0]; if ( fR > fR01 ) return false; // axis C0+t*B1 fR = static_cast<float>(fabs(akB[1].dot(kD))); fR0 = afEA[0]*aafAbsC[0][1]+afEA[1]*aafAbsC[1][1]+afEA[2]*aafAbsC[2][1]; fR01 = fR0 + afEB[1]; if ( fR > fR01 ) return false; // axis C0+t*B2 fR = static_cast<float>(fabs(akB[2].dot(kD))); fR0 = afEA[0]*aafAbsC[0][2]+afEA[1]*aafAbsC[1][2]+afEA[2]*aafAbsC[2][2]; fR01 = fR0 + afEB[2]; if ( fR > fR01 ) return false; // axis C0+t*A0xB0 fR = static_cast<float>(fabs(afAD[2]*aafC[1][0]-afAD[1]*aafC[2][0])); fR0 = afEA[1]*aafAbsC[2][0] + afEA[2]*aafAbsC[1][0]; fR1 = afEB[1]*aafAbsC[0][2] + afEB[2]*aafAbsC[0][1]; fR01 = fR0 + fR1; if ( fR > fR01 ) return false; // axis C0+t*A0xB1 fR = static_cast<float>(fabs(afAD[2]*aafC[1][1]-afAD[1]*aafC[2][1])); fR0 = afEA[1]*aafAbsC[2][1] + afEA[2]*aafAbsC[1][1]; fR1 = afEB[0]*aafAbsC[0][2] + afEB[2]*aafAbsC[0][0]; fR01 = fR0 + fR1; if ( fR > fR01 ) return false; // axis C0+t*A0xB2 fR = static_cast<float>(fabs(afAD[2]*aafC[1][2]-afAD[1]*aafC[2][2])); fR0 = afEA[1]*aafAbsC[2][2] + afEA[2]*aafAbsC[1][2]; fR1 = afEB[0]*aafAbsC[0][1] + afEB[1]*aafAbsC[0][0]; fR01 = fR0 + fR1; if ( fR > fR01 ) return false; // axis C0+t*A1xB0 fR = static_cast<float>(fabs(afAD[0]*aafC[2][0]-afAD[2]*aafC[0][0])); fR0 = afEA[0]*aafAbsC[2][0] + afEA[2]*aafAbsC[0][0]; fR1 = afEB[1]*aafAbsC[1][2] + afEB[2]*aafAbsC[1][1]; fR01 = fR0 + fR1; if ( fR > fR01 ) return false; // axis C0+t*A1xB1 fR = static_cast<float>(fabs(afAD[0]*aafC[2][1]-afAD[2]*aafC[0][1])); fR0 = afEA[0]*aafAbsC[2][1] + afEA[2]*aafAbsC[0][1]; fR1 = afEB[0]*aafAbsC[1][2] + afEB[2]*aafAbsC[1][0]; fR01 = fR0 + fR1; if ( fR > fR01 ) return false; // axis C0+t*A1xB2 fR = static_cast<float>(fabs(afAD[0]*aafC[2][2]-afAD[2]*aafC[0][2])); fR0 = afEA[0]*aafAbsC[2][2] + afEA[2]*aafAbsC[0][2]; fR1 = afEB[0]*aafAbsC[1][1] + afEB[1]*aafAbsC[1][0]; fR01 = fR0 + fR1; if ( fR > fR01 ) return false; // axis C0+t*A2xB0 fR = static_cast<float>(fabs(afAD[1]*aafC[0][0]-afAD[0]*aafC[1][0])); fR0 = afEA[0]*aafAbsC[1][0] + afEA[1]*aafAbsC[0][0]; fR1 = afEB[1]*aafAbsC[2][2] + afEB[2]*aafAbsC[2][1]; fR01 = fR0 + fR1; if ( fR > fR01 ) return false; // axis C0+t*A2xB1 fR = static_cast<float>(fabs(afAD[1]*aafC[0][1]-afAD[0]*aafC[1][1])); fR0 = afEA[0]*aafAbsC[1][1] + afEA[1]*aafAbsC[0][1]; fR1 = afEB[0]*aafAbsC[2][2] + afEB[2]*aafAbsC[2][0]; fR01 = fR0 + fR1; if ( fR > fR01 ) return false; // axis C0+t*A2xB2 fR = static_cast<float>(fabs(afAD[1]*aafC[0][2]-afAD[0]*aafC[1][2])); fR0 = afEA[0]*aafAbsC[1][2] + afEA[1]*aafAbsC[0][2]; fR1 = afEB[0]*aafAbsC[2][1] + afEB[1]*aafAbsC[2][0]; fR01 = fR0 + fR1; if ( fR > fR01 ) return false; return true; }
ContainmentResult TestPointOBox ( Vector const & V, OrientedBox const & B ) { Vector localV = B.transformToLocal(V); return Test(localV,B.getLocalShape()); }
ErrorCode TreeValidator::visit( EntityHandle node, int depth, bool& descend ) { ErrorCode rval; descend = true; Range contents; rval = instance->get_entities_by_handle( node, contents ); if (MB_SUCCESS != rval) return error(node, "Error getting contents of tree node. Corrupt tree?"); entity_count += contents.size(); if (surfaces) { // if no longer in subtree for previous surface, clear if (depth <= surface_depth) surface_depth = -1; EntityHandle surface = 0; Range::iterator surf_iter = contents.lower_bound( MBENTITYSET ); if (surf_iter != contents.end()) { surface = *surf_iter; contents.erase( surf_iter ); } if (surface) { if (surface_depth >=0) { ++multiple_surface_count; print( node, "Multiple surfaces in encountered in same subtree." ); } else { surface_depth = depth; surface_handle = surface; } } } std::vector<EntityHandle> children; rval = tool->get_moab_instance()->get_child_meshsets( node, children ); if (MB_SUCCESS != rval || (!children.empty() && children.size() != 2)) return error(node, "Error getting children. Corrupt tree?"); OrientedBox box; rval = tool->box( node, box ); if (MB_SUCCESS != rval) return error(node, "Error getting oriented box from tree node. Corrupt tree?"); if (children.empty() && contents.empty()) { ++empty_leaf_count; print( node, "Empty leaf node.\n" ); } else if (!children.empty() && !contents.empty()) { ++non_empty_non_leaf_count; print( node, "Non-leaf node is not empty." ); } if (surfaces && children.empty() && surface_depth < 0) { ++missing_surface_count; print( node, "Reached leaf node w/out encountering any surface set."); } double dot_epsilon = epsilon*(box.axis[0]+box.axis[1]+box.axis[2]).length(); if (box.axis[0] % box.axis[1] > dot_epsilon || box.axis[0] % box.axis[2] > dot_epsilon || box.axis[1] % box.axis[2] > dot_epsilon ) { ++non_ortho_count; print (node, "Box axes are not orthogonal"); } if (!children.empty()) { for (int i = 0; i < 2; ++i) { OrientedBox other_box; rval = tool->box( children[i], other_box ); if (MB_SUCCESS != rval) return error( children[i], " Error getting oriented box from tree node. Corrupt tree?" ); // else if (!box.contained( other_box, epsilon )) { // ++child_outside_count; // print( children[i], "Parent box does not contain child box." ); // char string[64]; // sprintf(string, " Volume ratio is %f", other_box.volume()/box.volume() ); // print( children [i], string ); // } else { double vol_ratio = other_box.volume()/box.volume(); if (vol_ratio > 2.0) { char string[64]; sprintf(string, "child/parent volume ratio is %f", vol_ratio ); print( children[i], string ); sprintf(string, " child/parent area ratio is %f", other_box.area()/box.area() ); print( children[i], string ); } } } } bool bad_element = false; bool bad_element_handle = false; bool bad_element_conn = false; bool duplicate_element = false; int num_outside = 0; bool boundary[6] = { false, false, false, false, false, false }; for (Range::iterator it = contents.begin(); it != contents.end(); ++it) { EntityType type = instance->type_from_handle( *it ); int dim = CN::Dimension( type ); if (dim != 2) { bad_element = true; continue; } const EntityHandle* conn; int conn_len; rval = instance->get_connectivity( *it, conn, conn_len ); if (MB_SUCCESS != rval) { bad_element_handle = true; continue; } std::vector<CartVect> coords(conn_len); rval = instance->get_coords( conn, conn_len, coords[0].array() ); if (MB_SUCCESS != rval) { bad_element_conn = true; continue; } bool outside = false; for (std::vector<CartVect>::iterator j = coords.begin(); j != coords.end(); ++j) { if (!box.contained( *j, epsilon )) outside = true; else for (int d = 0; d < 3; ++d) { #if MB_ORIENTED_BOX_UNIT_VECTORS double n = box.axis[d] % (*j - box.center); if (fabs(n - box.length[d]) <= epsilon) boundary[2*d] = true; if (fabs(n + box.length[d]) <= epsilon) boundary[2*d+1] = true; #else double ln = box.axis[d].length(); CartVect v1 = *j - box.center - box.axis[d]; CartVect v2 = *j - box.center + box.axis[d]; if (fabs(v1 % box.axis[d]) <= ln * epsilon) boundary[2*d] = true; if (fabs(v2 % box.axis[d]) <= ln * epsilon) boundary[2*d+1] = true; #endif } } if (outside) ++num_outside; if (!seen.insert(*it).second) { duplicate_element = true; ++duplicate_entity_count; } } CartVect alength( box.axis[0].length(), box.axis[1].length(), box.axis[2].length() ); #if MB_ORIENTED_BOX_UNIT_VECTORS CartVect length = box.length; #else CartVect length = alength; #endif if (length[0] > length[1] || length[0] > length[2] || length[1] > length[2]) { ++unsorted_axis_count; print( node, "Box axes are not ordered from shortest to longest." ); } #if MB_ORIENTED_BOX_UNIT_VECTORS if (fabs(alength[0] - 1.0) > epsilon || fabs(alength[1] - 1.0) > epsilon || fabs(alength[2] - 1.0) > epsilon) { ++non_unit_count; print( node, "Box axes are not unit vectors."); } #endif #if MB_ORIENTED_BOX_OUTER_RADIUS if (fabs(length.length() - box.radius) > tolerance) { ++bad_outer_radius_count; print( node, "Box has incorrect outer radius."); } #endif if (depth+1 < settings.max_depth && contents.size() > (unsigned)(4*settings.max_leaf_entities)) { char string[64]; sprintf(string, "leaf at depth %d with %u entities", depth, (unsigned)contents.size() ); print( node, string ); } bool all_boundaries = true; for (int f = 0; f < 6; ++f) all_boundaries = all_boundaries && boundary[f]; if (bad_element) { ++entity_invalid_count; print( node, "Set contained an entity with an inappropriate dimension." ); } if (bad_element_handle) { ++error_count; print( node, "Error querying face contained in set."); } if (bad_element_conn) { ++error_count; print( node, "Error querying connectivity of element."); } if (duplicate_element) { print( node, "Elements occur in multiple leaves of tree."); } if (num_outside > 0) { ++entity_outside_count; num_entities_outside += num_outside; if (printing) stream << instance->id_from_handle( node ) << ": " << num_outside << " elements outside box." << std::endl; } else if (!all_boundaries && !contents.empty()) { ++loose_box_count; print( node, "Box does not fit contained elements tightly." ); } return MB_SUCCESS; }
DEMParticlePArray DEMParticleCreator::updatePeriodicDEMParticles(const OrientedBox& periodicDomain, DEMParticlePArray& particles) { auto e0 = periodicDomain.axis(0) * (periodicDomain.extent(0) * 2); auto e1 = periodicDomain.axis(1) * (periodicDomain.extent(1) * 2); auto e2 = periodicDomain.axis(2) * (periodicDomain.extent(2) * 2); std::vector<Vec> vertices = periodicDomain.vertices(); constexpr std::array<std::array<int, 4>, 6> faceIndices = {{ {{0, 4, 7, 3}} // x- , {{1, 2, 6, 5}} // x+ , {{0, 1, 5, 4}} // y- , {{2, 3, 7, 6}} // y+ , {{0, 3, 2, 1}} // z- , {{4, 5, 6, 7}} // z+ }}; std::vector<Face> faces; for (const auto& indices : faceIndices) { int i0 = indices[0]; int i1 = indices[1]; int i2 = indices[2]; int i3 = indices[3]; Face face(vertices[i0], vertices[i1], vertices[i2], vertices[i3]); faces.push_back(face); } // Check intersections DEMParticlePArray extraParticles; for (const auto& particle : particles) { if (particle->getType() != DEMParticle::DEMParticleType::FREE) { continue; } auto position = particle->currentPosition(); auto axis_a = particle->currentAxisA(); auto axis_b = particle->currentAxisB(); auto axis_c = particle->currentAxisC(); auto radius_a = particle->radiusA(); auto radius_b = particle->radiusB(); auto radius_c = particle->radiusC(); auto id = particle->getId(); // Create an ellipsoid object for ellipsoid-face intersection tests Ellipsoid ellipsoid(id, position, axis_a, axis_b, axis_c, radius_a, radius_b, radius_c); int faceID = 1; for (const auto& face : faces) { auto status = ellipsoid.intersects(face); // std::cout << "Face = " << face << "\n"; // std::cout << "status = " << std::boolalpha << status.first // << " face " << static_cast<int>(status.second.first) // << " , " << status.second.second << "\n"; if (status.first) { std::vector<Vec> translations; switch (static_cast<Boundary::BoundaryID>(faceID)) { case Boundary::BoundaryID::NONE: break; case Boundary::BoundaryID::XMINUS: { Vec shift = e0; //std::cout << " Loc: x-: " << shift << "\n"; translations.push_back(shift); addExtraTranslations(shift, face, status, translations); break; } case Boundary::BoundaryID::XPLUS: { Vec shift = -e0; //std::cout << " Loc: x-: " << shift << "\n"; translations.push_back(shift); addExtraTranslations(shift, face, status, translations); break; } case Boundary::BoundaryID::YMINUS: { Vec shift = e1; //std::cout << " Loc: y-: " << shift << "\n"; translations.push_back(shift); addExtraTranslations(shift, face, status, translations); break; } case Boundary::BoundaryID::YPLUS: { Vec shift = -e1; //std::cout << " Loc: y-: " << shift << "\n"; translations.push_back(shift); addExtraTranslations(shift, face, status, translations); break; } case Boundary::BoundaryID::ZMINUS: { Vec shift = e2; //std::cout << " Loc: z-: " << shift << "\n"; translations.push_back(shift); addExtraTranslations(shift, face, status, translations); break; } case Boundary::BoundaryID::ZPLUS: { Vec shift = -e2; //std::cout << " Loc: z-: " << shift << "\n"; translations.push_back(shift); addExtraTranslations(shift, face, status, translations); break; } break; } // Create copies particle->setType(DEMParticle::DEMParticleType::BOUNDARY_PERIODIC); for (const auto& translation : translations) { DEMParticleP newParticle = std::make_shared<DEMParticle>(*particle); newParticle->setCurrentPosition(particle->currentPosition() + translation); newParticle->setPreviousPosition(particle->previousPosition() + translation); newParticle->setId(particle->getId()); newParticle->computeAndSetGlobalCoef(); extraParticles.push_back(newParticle); } } faceID += 1; } } // Remove duplicates removeDuplicates(extraParticles); return extraParticles; }