UNPKG

@babylonjs/core

Version:

Getting started? Play directly with the Babylon.js API using our [playground](https://playground.babylonjs.com/). It also contains a lot of samples to learn how to use it.

1,004 lines (1,003 loc) 86.8 kB
import { Vector3, Matrix, TmpVectors, Quaternion } from "../Maths/math.vector.js"; import { Color4 } from "../Maths/math.color.js"; import { VertexBuffer } from "../Buffers/buffer.js"; import { VertexData } from "../Meshes/mesh.vertexData.js"; import { Mesh } from "../Meshes/mesh.js"; import { CreateDisc } from "../Meshes/Builders/discBuilder.js"; import { EngineStore } from "../Engines/engineStore.js"; import { DepthSortedParticle, SolidParticle, ModelShape, SolidParticleVertex } from "./solidParticle.js"; import { BoundingInfo } from "../Culling/boundingInfo.js"; import { Axis } from "../Maths/math.axis.js"; import { SubMesh } from "../Meshes/subMesh.js"; import { StandardMaterial } from "../Materials/standardMaterial.js"; import { MultiMaterial } from "../Materials/multiMaterial.js"; /** * The SPS is a single updatable mesh. The solid particles are simply separate parts or faces of this big mesh. *As it is just a mesh, the SPS has all the same properties than any other BJS mesh : not more, not less. It can be scaled, rotated, translated, enlighted, textured, moved, etc. * The SPS is also a particle system. It provides some methods to manage the particles. * However it is behavior agnostic. This means it has no emitter, no particle physics, no particle recycler. You have to implement your own behavior. * * Full documentation here : https://doc.babylonjs.com/features/featuresDeepDive/particles/solid_particle_system/sps_intro */ export class SolidParticleSystem { /** * Creates a SPS (Solid Particle System) object. * @param name (String) is the SPS name, this will be the underlying mesh name. * @param scene (Scene) is the scene in which the SPS is added. * @param options defines the options of the sps e.g. * * updatable (optional boolean, default true) : if the SPS must be updatable or immutable. * * isPickable (optional boolean, default false) : if the solid particles must be pickable. * * enableDepthSort (optional boolean, default false) : if the solid particles must be sorted in the geometry according to their distance to the camera. * * useModelMaterial (optional boolean, default false) : if the model materials must be used to create the SPS multimaterial. This enables the multimaterial supports of the SPS. * * enableMultiMaterial (optional boolean, default false) : if the solid particles can be given different materials. * * expandable (optional boolean, default false) : if particles can still be added after the initial SPS mesh creation. * * particleIntersection (optional boolean, default false) : if the solid particle intersections must be computed. * * boundingSphereOnly (optional boolean, default false) : if the particle intersection must be computed only with the bounding sphere (no bounding box computation, so faster). * * bSphereRadiusFactor (optional float, default 1.0) : a number to multiply the bounding sphere radius by in order to reduce it for instance. * * computeBoundingBox (optional boolean, default false): if the bounding box of the entire SPS will be computed (for occlusion detection, for example). If it is false, the bounding box will be the bounding box of the first particle. * * autoFixFaceOrientation (optional boolean, default false): if the particle face orientations will be flipped for transformations that change orientation (scale (-1, 1, 1), for example) * @param options.updatable * @param options.isPickable * @param options.enableDepthSort * @param options.particleIntersection * @param options.boundingSphereOnly * @param options.bSphereRadiusFactor * @param options.expandable * @param options.useModelMaterial * @param options.enableMultiMaterial * @param options.computeBoundingBox * @param options.autoFixFaceOrientation * @example bSphereRadiusFactor = 1.0 / Math.sqrt(3.0) => the bounding sphere exactly matches a spherical mesh. */ constructor(name, scene, options) { /** * The SPS array of Solid Particle objects. Just access each particle as with any classic array. * Example : var p = SPS.particles[i]; */ this.particles = new Array(); /** * The SPS total number of particles. Read only. Use SPS.counter instead if you need to set your own value. */ this.nbParticles = 0; /** * If the particles must ever face the camera (default false). Useful for planar particles. */ this.billboard = false; /** * Recompute normals when adding a shape */ this.recomputeNormals = false; /** * This a counter ofr your own usage. It's not set by any SPS functions. */ this.counter = 0; /** * This empty object is intended to store some SPS specific or temporary values in order to lower the Garbage Collector activity. * Please read : https://doc.babylonjs.com/features/featuresDeepDive/particles/solid_particle_system/optimize_sps#limit-garbage-collection */ this.vars = {}; /** * If the particle intersection must be computed only with the bounding sphere (no bounding box computation, so faster). (Internal use only) * @internal */ this._bSphereOnly = false; /** * A number to multiply the bounding sphere radius by in order to reduce it for instance. (Internal use only) * @internal */ this._bSphereRadiusFactor = 1.0; this._positions = new Array(); this._indices = new Array(); this._normals = new Array(); this._colors = new Array(); this._uvs = new Array(); this._index = 0; // indices index this._updatable = true; this._pickable = false; this._isVisibilityBoxLocked = false; this._alwaysVisible = false; this._depthSort = false; this._expandable = false; this._shapeCounter = 0; this._copy = new SolidParticle(0, 0, 0, 0, null, 0, 0, this); this._color = new Color4(0, 0, 0, 0); this._computeParticleColor = true; this._computeParticleTexture = true; this._computeParticleRotation = true; this._computeParticleVertex = false; this._computeBoundingBox = false; this._autoFixFaceOrientation = false; this._depthSortParticles = true; this._mustUnrotateFixedNormals = false; this._particlesIntersect = false; this._needs32Bits = false; this._isNotBuilt = true; this._lastParticleId = 0; this._idxOfId = []; // array : key = particle.id / value = particle.idx this._multimaterialEnabled = false; this._useModelMaterial = false; this._depthSortFunction = (p1, p2) => p2.sqDistance - p1.sqDistance; this._materialSortFunction = (p1, p2) => p1.materialIndex - p2.materialIndex; this._autoUpdateSubMeshes = false; this._recomputeInvisibles = false; this.name = name; this._scene = scene || EngineStore.LastCreatedScene; this._camera = scene.activeCamera; this._pickable = options ? options.isPickable : false; this._depthSort = options ? options.enableDepthSort : false; this._multimaterialEnabled = options ? options.enableMultiMaterial : false; this._useModelMaterial = options ? options.useModelMaterial : false; this._multimaterialEnabled = this._useModelMaterial ? true : this._multimaterialEnabled; this._expandable = options ? options.expandable : false; this._particlesIntersect = options ? options.particleIntersection : false; this._bSphereOnly = options ? options.boundingSphereOnly : false; this._bSphereRadiusFactor = options && options.bSphereRadiusFactor ? options.bSphereRadiusFactor : 1.0; this._computeBoundingBox = options?.computeBoundingBox ? options.computeBoundingBox : false; this._autoFixFaceOrientation = options?.autoFixFaceOrientation ? options.autoFixFaceOrientation : false; if (options && options.updatable !== undefined) { this._updatable = options.updatable; } else { this._updatable = true; } if (this._pickable) { this.pickedBySubMesh = [[]]; this.pickedParticles = this.pickedBySubMesh[0]; } if (this._depthSort || this._multimaterialEnabled) { this.depthSortedParticles = []; } if (this._multimaterialEnabled) { this._multimaterial = new MultiMaterial(this.name + "MultiMaterial", this._scene); this._materials = []; this._materialIndexesById = {}; } this._tmpVertex = new SolidParticleVertex(); } /** * Builds the SPS underlying mesh. Returns a standard Mesh. * If no model shape was added to the SPS, the returned mesh is just a single triangular plane. * @returns the created mesh */ buildMesh() { if (!this._isNotBuilt && this.mesh) { return this.mesh; } if (this.nbParticles === 0 && !this.mesh) { const triangle = CreateDisc("", { radius: 1, tessellation: 3 }, this._scene); this.addShape(triangle, 1); triangle.dispose(); } this._indices32 = this._needs32Bits ? new Uint32Array(this._indices) : new Uint16Array(this._indices); this._positions32 = new Float32Array(this._positions); this._uvs32 = new Float32Array(this._uvs); this._colors32 = new Float32Array(this._colors); if (!this.mesh) { // in case it's already expanded const mesh = new Mesh(this.name, this._scene); this.mesh = mesh; } if (!this._updatable && this._multimaterialEnabled) { this._sortParticlesByMaterial(); // this may reorder the indices32 } if (this.recomputeNormals) { VertexData.ComputeNormals(this._positions32, this._indices32, this._normals); } this._normals32 = new Float32Array(this._normals); this._fixedNormal32 = new Float32Array(this._normals); if (this._mustUnrotateFixedNormals) { // the particles could be created already rotated in the mesh with a positionFunction this._unrotateFixedNormals(); } const vertexData = new VertexData(); vertexData.indices = this._depthSort ? this._indices : this._indices32; vertexData.set(this._positions32, VertexBuffer.PositionKind); vertexData.set(this._normals32, VertexBuffer.NormalKind); if (this._uvs32.length > 0) { vertexData.set(this._uvs32, VertexBuffer.UVKind); } if (this._colors32.length > 0) { vertexData.set(this._colors32, VertexBuffer.ColorKind); } vertexData.applyToMesh(this.mesh, this._updatable); this.mesh.isPickable = this._pickable; if (this._pickable) { let faceId = 0; for (let p = 0; p < this.nbParticles; p++) { const part = this.particles[p]; const lind = part._model._indicesLength; for (let i = 0; i < lind; i++) { const f = i % 3; if (f == 0) { const pickedData = { idx: part.idx, faceId: faceId }; this.pickedParticles[faceId] = pickedData; faceId++; } } } } if (this._multimaterialEnabled) { this.setMultiMaterial(this._materials); } if (!this._expandable) { // free memory if (!this._depthSort && !this._multimaterialEnabled && !this._autoFixFaceOrientation) { this._indices = null; } this._positions = null; this._normals = null; this._uvs = null; this._colors = null; if (!this._updatable) { this.particles.length = 0; } } this._isNotBuilt = false; this.recomputeNormals = false; this._recomputeInvisibles = true; return this.mesh; } _getUVKind(mesh, uvKind) { if (uvKind === -1) { if (mesh.material?.diffuseTexture) { uvKind = mesh.material.diffuseTexture.coordinatesIndex; } else if (mesh.material?.albedoTexture) { uvKind = mesh.material.albedoTexture.coordinatesIndex; } } return "uv" + (uvKind ? uvKind + 1 : ""); } /** * Digests the mesh and generates as many solid particles in the system as wanted. Returns the SPS. * These particles will have the same geometry than the mesh parts and will be positioned at the same localisation than the mesh original places. * Thus the particles generated from `digest()` have their property `position` set yet. * @param mesh ( Mesh ) is the mesh to be digested * @param options {facetNb} (optional integer, default 1) is the number of mesh facets per particle, this parameter is overridden by the parameter `number` if any * {delta} (optional integer, default 0) is the random extra number of facets per particle , each particle will have between `facetNb` and `facetNb + delta` facets * {number} (optional positive integer) is the wanted number of particles : each particle is built with `mesh_total_facets / number` facets * {storage} (optional existing array) is an array where the particles will be stored for a further use instead of being inserted in the SPS. * {uvKind} (optional positive integer, default 0) is the kind of UV to read from. Use -1 to deduce it from the diffuse/albedo texture (if any) of the mesh material * @param options.facetNb * @param options.number * @param options.delta * @param options.storage * @param options.uvKind * @returns the current SPS */ digest(mesh, options) { let size = (options && options.facetNb) || 1; let number = (options && options.number) || 0; let delta = (options && options.delta) || 0; const meshPos = mesh.getVerticesData(VertexBuffer.PositionKind); const meshInd = mesh.getIndices(); const meshUV = mesh.getVerticesData(this._getUVKind(mesh, options?.uvKind ?? 0)); const meshCol = mesh.getVerticesData(VertexBuffer.ColorKind); const meshNor = mesh.getVerticesData(VertexBuffer.NormalKind); const storage = options && options.storage ? options.storage : null; let f = 0; // facet counter const totalFacets = meshInd.length / 3; // a facet is a triangle, so 3 indices // compute size from number if (number) { number = number > totalFacets ? totalFacets : number; size = Math.round(totalFacets / number); delta = 0; } else { size = size > totalFacets ? totalFacets : size; } const facetPos = []; // submesh positions const facetNor = []; const facetInd = []; // submesh indices const facetUV = []; // submesh UV const facetCol = []; // submesh colors const barycenter = Vector3.Zero(); const sizeO = size; while (f < totalFacets) { size = sizeO + Math.floor((1 + delta) * Math.random()); if (f > totalFacets - size) { size = totalFacets - f; } // reset temp arrays facetPos.length = 0; facetNor.length = 0; facetInd.length = 0; facetUV.length = 0; facetCol.length = 0; // iterate over "size" facets let fi = 0; for (let j = f * 3; j < (f + size) * 3; j++) { facetInd.push(fi); const i = meshInd[j]; const i3 = i * 3; facetPos.push(meshPos[i3], meshPos[i3 + 1], meshPos[i3 + 2]); facetNor.push(meshNor[i3], meshNor[i3 + 1], meshNor[i3 + 2]); if (meshUV) { const i2 = i * 2; facetUV.push(meshUV[i2], meshUV[i2 + 1]); } if (meshCol) { const i4 = i * 4; facetCol.push(meshCol[i4], meshCol[i4 + 1], meshCol[i4 + 2], meshCol[i4 + 3]); } fi++; } // create a model shape for each single particle let idx = this.nbParticles; const shape = this._posToShape(facetPos); const shapeUV = this._uvsToShapeUV(facetUV); const shapeInd = facetInd.slice(); const shapeCol = facetCol.slice(); const shapeNor = facetNor.slice(); // compute the barycenter of the shape barycenter.copyFromFloats(0, 0, 0); let v; for (v = 0; v < shape.length; v++) { barycenter.addInPlace(shape[v]); } barycenter.scaleInPlace(1 / shape.length); // shift the shape from its barycenter to the origin // and compute the BBox required for intersection. const minimum = new Vector3(Infinity, Infinity, Infinity); const maximum = new Vector3(-Infinity, -Infinity, -Infinity); for (v = 0; v < shape.length; v++) { shape[v].subtractInPlace(barycenter); minimum.minimizeInPlaceFromFloats(shape[v].x, shape[v].y, shape[v].z); maximum.maximizeInPlaceFromFloats(shape[v].x, shape[v].y, shape[v].z); } let bInfo; if (this._particlesIntersect) { bInfo = new BoundingInfo(minimum, maximum); } let material = null; if (this._useModelMaterial) { material = mesh.material ? mesh.material : this._setDefaultMaterial(); } const modelShape = new ModelShape(this._shapeCounter, shape, shapeInd, shapeNor, shapeCol, shapeUV, null, null, material); // add the particle in the SPS const currentPos = this._positions.length; const currentInd = this._indices.length; this._meshBuilder(this._index, currentInd, shape, this._positions, shapeInd, this._indices, facetUV, this._uvs, shapeCol, this._colors, shapeNor, this._normals, idx, 0, null, modelShape); this._addParticle(idx, this._lastParticleId, currentPos, currentInd, modelShape, this._shapeCounter, 0, bInfo, storage); // initialize the particle position this.particles[this.nbParticles].position.addInPlace(barycenter); if (!storage) { this._index += shape.length; idx++; this.nbParticles++; this._lastParticleId++; } this._shapeCounter++; f += size; } this._isNotBuilt = true; // buildMesh() is now expected for setParticles() to work return this; } /** * Unrotate the fixed normals in case the mesh was built with pre-rotated particles, ex : use of positionFunction in addShape() * @internal */ _unrotateFixedNormals() { let index = 0; let idx = 0; const tmpNormal = TmpVectors.Vector3[0]; const quaternion = TmpVectors.Quaternion[0]; const invertedRotMatrix = TmpVectors.Matrix[0]; for (let p = 0; p < this.particles.length; p++) { const particle = this.particles[p]; const shape = particle._model._shape; // computing the inverse of the rotation matrix from the quaternion // is equivalent to computing the matrix of the inverse quaternion, i.e of the conjugate quaternion if (particle.rotationQuaternion) { particle.rotationQuaternion.conjugateToRef(quaternion); } else { const rotation = particle.rotation; Quaternion.RotationYawPitchRollToRef(rotation.y, rotation.x, rotation.z, quaternion); quaternion.conjugateInPlace(); } quaternion.toRotationMatrix(invertedRotMatrix); for (let pt = 0; pt < shape.length; pt++) { idx = index + pt * 3; Vector3.TransformNormalFromFloatsToRef(this._normals32[idx], this._normals32[idx + 1], this._normals32[idx + 2], invertedRotMatrix, tmpNormal); tmpNormal.toArray(this._fixedNormal32, idx); } index = idx + 3; } } /** * Resets the temporary working copy particle * @internal */ _resetCopy() { const copy = this._copy; copy.position.setAll(0); copy.rotation.setAll(0); copy.rotationQuaternion = null; copy.scaling.setAll(1); copy.uvs.copyFromFloats(0.0, 0.0, 1.0, 1.0); copy.color = null; copy.translateFromPivot = false; copy.shapeId = 0; copy.materialIndex = null; } /** * Inserts the shape model geometry in the global SPS mesh by updating the positions, indices, normals, colors, uvs arrays * @param p the current index in the positions array to be updated * @param ind the current index in the indices array * @param shape a Vector3 array, the shape geometry * @param positions the positions array to be updated * @param meshInd the shape indices array * @param indices the indices array to be updated * @param meshUV the shape uv array * @param uvs the uv array to be updated * @param meshCol the shape color array * @param colors the color array to be updated * @param meshNor the shape normals array * @param normals the normals array to be updated * @param idx the particle index * @param idxInShape the particle index in its shape * @param options the addShape() method passed options * @param model * @model the particle model * @internal */ _meshBuilder(p, ind, shape, positions, meshInd, indices, meshUV, uvs, meshCol, colors, meshNor, normals, idx, idxInShape, options, model) { let i; let u = 0; let c = 0; let n = 0; this._resetCopy(); const copy = this._copy; const storeApart = options && options.storage ? true : false; copy.idx = idx; copy.idxInShape = idxInShape; copy.shapeId = model.shapeId; if (this._useModelMaterial) { const materialId = model._material.uniqueId; const materialIndexesById = this._materialIndexesById; if (!Object.prototype.hasOwnProperty.call(materialIndexesById, materialId)) { materialIndexesById[materialId] = this._materials.length; this._materials.push(model._material); } const matIdx = materialIndexesById[materialId]; copy.materialIndex = matIdx; } if (options && options.positionFunction) { // call to custom positionFunction options.positionFunction(copy, idx, idxInShape); this._mustUnrotateFixedNormals = true; } // in case the particle geometry must NOT be inserted in the SPS mesh geometry if (storeApart) { return copy; } const rotMatrix = TmpVectors.Matrix[0]; const tmpVertex = this._tmpVertex; const tmpVector = tmpVertex.position; const tmpColor = tmpVertex.color; const tmpUV = tmpVertex.uv; const tmpRotated = TmpVectors.Vector3[1]; const pivotBackTranslation = TmpVectors.Vector3[2]; const scaledPivot = TmpVectors.Vector3[3]; Matrix.IdentityToRef(rotMatrix); copy.getRotationMatrix(rotMatrix); copy.pivot.multiplyToRef(copy.scaling, scaledPivot); if (copy.translateFromPivot) { pivotBackTranslation.setAll(0.0); } else { pivotBackTranslation.copyFrom(scaledPivot); } const someVertexFunction = options && options.vertexFunction; for (i = 0; i < shape.length; i++) { tmpVector.copyFrom(shape[i]); if (copy.color) { tmpColor.copyFrom(copy.color); } if (meshUV) { tmpUV.copyFromFloats(meshUV[u], meshUV[u + 1]); } if (someVertexFunction) { options.vertexFunction(copy, tmpVertex, i); } tmpVector.multiplyInPlace(copy.scaling).subtractInPlace(scaledPivot); Vector3.TransformCoordinatesToRef(tmpVector, rotMatrix, tmpRotated); tmpRotated.addInPlace(pivotBackTranslation).addInPlace(copy.position); positions.push(tmpRotated.x, tmpRotated.y, tmpRotated.z); if (meshUV) { const copyUvs = copy.uvs; uvs.push((copyUvs.z - copyUvs.x) * tmpUV.x + copyUvs.x, (copyUvs.w - copyUvs.y) * tmpUV.y + copyUvs.y); u += 2; } if (copy.color) { this._color.copyFrom(tmpColor); } else { const color = this._color; if (meshCol && meshCol[c] !== undefined) { color.r = meshCol[c]; color.g = meshCol[c + 1]; color.b = meshCol[c + 2]; color.a = meshCol[c + 3]; } else { color.r = 1.0; color.g = 1.0; color.b = 1.0; color.a = 1.0; } } colors.push(this._color.r, this._color.g, this._color.b, this._color.a); c += 4; if (!this.recomputeNormals && meshNor) { Vector3.TransformNormalFromFloatsToRef(meshNor[n], meshNor[n + 1], meshNor[n + 2], rotMatrix, tmpVector); normals.push(tmpVector.x, tmpVector.y, tmpVector.z); n += 3; } } for (i = 0; i < meshInd.length; i++) { const currentInd = p + meshInd[i]; indices.push(currentInd); if (currentInd > 65535) { this._needs32Bits = true; } } if (this._depthSort || this._multimaterialEnabled) { const matIndex = copy.materialIndex !== null ? copy.materialIndex : 0; this.depthSortedParticles.push(new DepthSortedParticle(idx, ind, meshInd.length, matIndex)); } return copy; } /** * Returns a shape Vector3 array from positions float array * @param positions float array * @returns a vector3 array * @internal */ _posToShape(positions) { const shape = []; for (let i = 0; i < positions.length; i += 3) { shape.push(Vector3.FromArray(positions, i)); } return shape; } /** * Returns a shapeUV array from a float uvs (array deep copy) * @param uvs as a float array * @returns a shapeUV array * @internal */ _uvsToShapeUV(uvs) { const shapeUV = []; if (uvs) { for (let i = 0; i < uvs.length; i++) { shapeUV.push(uvs[i]); } } return shapeUV; } /** * Adds a new particle object in the particles array * @param idx particle index in particles array * @param id particle id * @param idxpos positionIndex : the starting index of the particle vertices in the SPS "positions" array * @param idxind indiceIndex : he starting index of the particle indices in the SPS "indices" array * @param model particle ModelShape object * @param shapeId model shape identifier * @param idxInShape index of the particle in the current model * @param bInfo model bounding info object * @param storage target storage array, if any * @internal */ _addParticle(idx, id, idxpos, idxind, model, shapeId, idxInShape, bInfo = null, storage = null) { const sp = new SolidParticle(idx, id, idxpos, idxind, model, shapeId, idxInShape, this, bInfo); const target = storage ? storage : this.particles; target.push(sp); return sp; } /** * Adds some particles to the SPS from the model shape. Returns the shape id. * Please read the doc : https://doc.babylonjs.com/features/featuresDeepDive/particles/solid_particle_system/immutable_sps * @param mesh is any Mesh object that will be used as a model for the solid particles. If the mesh does not have vertex normals, it will turn on the recomputeNormals attribute. * @param nb (positive integer) the number of particles to be created from this model * @param options {positionFunction} is an optional javascript function to called for each particle on SPS creation. * {vertexFunction} is an optional javascript function to called for each vertex of each particle on SPS creation * {storage} (optional existing array) is an array where the particles will be stored for a further use instead of being inserted in the SPS. * @param options.positionFunction * @param options.vertexFunction * @param options.storage * @returns the number of shapes in the system */ addShape(mesh, nb, options) { const meshPos = mesh.getVerticesData(VertexBuffer.PositionKind); const meshInd = mesh.getIndices(); const meshUV = mesh.getVerticesData(VertexBuffer.UVKind); const meshCol = mesh.getVerticesData(VertexBuffer.ColorKind); const meshNor = mesh.getVerticesData(VertexBuffer.NormalKind); this.recomputeNormals = meshNor ? false : true; const indices = Array.from(meshInd); const shapeNormals = meshNor ? Array.from(meshNor) : []; const shapeColors = meshCol ? Array.from(meshCol) : []; const storage = options && options.storage ? options.storage : null; let bbInfo = null; if (this._particlesIntersect) { bbInfo = mesh.getBoundingInfo(); } const shape = this._posToShape(meshPos); const shapeUV = this._uvsToShapeUV(meshUV); const posfunc = options ? options.positionFunction : null; const vtxfunc = options ? options.vertexFunction : null; let material = null; if (this._useModelMaterial) { material = mesh.material ? mesh.material : this._setDefaultMaterial(); } const modelShape = new ModelShape(this._shapeCounter, shape, indices, shapeNormals, shapeColors, shapeUV, posfunc, vtxfunc, material); // particles for (let i = 0; i < nb; i++) { this._insertNewParticle(this.nbParticles, i, modelShape, shape, meshInd, meshUV, meshCol, meshNor, bbInfo, storage, options); } this._shapeCounter++; this._isNotBuilt = true; // buildMesh() call is now expected for setParticles() to work return this._shapeCounter - 1; } /** * Rebuilds a particle back to its just built status : if needed, recomputes the custom positions and vertices * @internal */ _rebuildParticle(particle, reset = false) { this._resetCopy(); const copy = this._copy; if (particle._model._positionFunction) { // recall to stored custom positionFunction particle._model._positionFunction(copy, particle.idx, particle.idxInShape); } const rotMatrix = TmpVectors.Matrix[0]; const tmpVertex = TmpVectors.Vector3[0]; const tmpRotated = TmpVectors.Vector3[1]; const pivotBackTranslation = TmpVectors.Vector3[2]; const scaledPivot = TmpVectors.Vector3[3]; copy.getRotationMatrix(rotMatrix); particle.pivot.multiplyToRef(particle.scaling, scaledPivot); if (copy.translateFromPivot) { pivotBackTranslation.copyFromFloats(0.0, 0.0, 0.0); } else { pivotBackTranslation.copyFrom(scaledPivot); } const shape = particle._model._shape; for (let pt = 0; pt < shape.length; pt++) { tmpVertex.copyFrom(shape[pt]); if (particle._model._vertexFunction) { particle._model._vertexFunction(copy, tmpVertex, pt); // recall to stored vertexFunction } tmpVertex.multiplyInPlace(copy.scaling).subtractInPlace(scaledPivot); Vector3.TransformCoordinatesToRef(tmpVertex, rotMatrix, tmpRotated); tmpRotated .addInPlace(pivotBackTranslation) .addInPlace(copy.position) .toArray(this._positions32, particle._pos + pt * 3); } if (reset) { particle.position.setAll(0.0); particle.rotation.setAll(0.0); particle.rotationQuaternion = null; particle.scaling.setAll(1.0); particle.uvs.setAll(0.0); particle.pivot.setAll(0.0); particle.translateFromPivot = false; particle.parentId = null; } } /** * Rebuilds the whole mesh and updates the VBO : custom positions and vertices are recomputed if needed. * @param reset boolean, default false : if the particles must be reset at position and rotation zero, scaling 1, color white, initial UVs and not parented. * @returns the SPS. */ rebuildMesh(reset = false) { for (let p = 0; p < this.particles.length; p++) { this._rebuildParticle(this.particles[p], reset); } this.mesh.updateVerticesData(VertexBuffer.PositionKind, this._positions32, false, false); return this; } /** Removes the particles from the start-th to the end-th included from an expandable SPS (required). * Returns an array with the removed particles. * If the number of particles to remove is lower than zero or greater than the global remaining particle number, then an empty array is returned. * The SPS can't be empty so at least one particle needs to remain in place. * Under the hood, the VertexData array, so the VBO buffer, is recreated each call. * @param start index of the first particle to remove * @param end index of the last particle to remove (included) * @returns an array populated with the removed particles */ removeParticles(start, end) { const nb = end - start + 1; if (!this._expandable || nb <= 0 || nb >= this.nbParticles || !this._updatable) { return []; } const particles = this.particles; const currentNb = this.nbParticles; if (end < currentNb - 1) { // update the particle indexes in the positions array in case they're remaining particles after the last removed const firstRemaining = end + 1; const shiftPos = particles[firstRemaining]._pos - particles[start]._pos; const shifInd = particles[firstRemaining]._ind - particles[start]._ind; for (let i = firstRemaining; i < currentNb; i++) { const part = particles[i]; part._pos -= shiftPos; part._ind -= shifInd; } } const removed = particles.splice(start, nb); this._positions.length = 0; this._indices.length = 0; this._colors.length = 0; this._uvs.length = 0; this._normals.length = 0; this._index = 0; this._idxOfId.length = 0; if (this._depthSort || this._multimaterialEnabled) { this.depthSortedParticles = []; } let ind = 0; const particlesLength = particles.length; for (let p = 0; p < particlesLength; p++) { const particle = particles[p]; const model = particle._model; const shape = model._shape; const modelIndices = model._indices; const modelNormals = model._normals; const modelColors = model._shapeColors; const modelUVs = model._shapeUV; particle.idx = p; this._idxOfId[particle.id] = p; this._meshBuilder(this._index, ind, shape, this._positions, modelIndices, this._indices, modelUVs, this._uvs, modelColors, this._colors, modelNormals, this._normals, particle.idx, particle.idxInShape, null, model); this._index += shape.length; ind += modelIndices.length; } this.nbParticles -= nb; this._isNotBuilt = true; // buildMesh() call is now expected for setParticles() to work return removed; } /** * Inserts some pre-created particles in the solid particle system so that they can be managed by setParticles(). * @param solidParticleArray an array populated with Solid Particles objects * @returns the SPS */ insertParticlesFromArray(solidParticleArray) { if (!this._expandable) { return this; } let idxInShape = 0; let currentShapeId = solidParticleArray[0].shapeId; const nb = solidParticleArray.length; for (let i = 0; i < nb; i++) { const sp = solidParticleArray[i]; const model = sp._model; const shape = model._shape; const meshInd = model._indices; const meshUV = model._shapeUV; const meshCol = model._shapeColors; const meshNor = model._normals; const noNor = meshNor ? false : true; this.recomputeNormals = noNor || this.recomputeNormals; const bbInfo = sp.getBoundingInfo(); const newPart = this._insertNewParticle(this.nbParticles, idxInShape, model, shape, meshInd, meshUV, meshCol, meshNor, bbInfo, null, null); sp.copyToRef(newPart); idxInShape++; if (currentShapeId != sp.shapeId) { currentShapeId = sp.shapeId; idxInShape = 0; } } this._isNotBuilt = true; // buildMesh() call is now expected for setParticles() to work return this; } /** * Creates a new particle and modifies the SPS mesh geometry : * - calls _meshBuilder() to increase the SPS mesh geometry step by step * - calls _addParticle() to populate the particle array * factorized code from addShape() and insertParticlesFromArray() * @param idx particle index in the particles array * @param i particle index in its shape * @param modelShape particle ModelShape object * @param shape shape vertex array * @param meshInd shape indices array * @param meshUV shape uv array * @param meshCol shape color array * @param meshNor shape normals array * @param bbInfo shape bounding info * @param storage target particle storage * @param options * @options addShape() passed options * @internal */ _insertNewParticle(idx, i, modelShape, shape, meshInd, meshUV, meshCol, meshNor, bbInfo, storage, options) { const currentPos = this._positions.length; const currentInd = this._indices.length; const currentCopy = this._meshBuilder(this._index, currentInd, shape, this._positions, meshInd, this._indices, meshUV, this._uvs, meshCol, this._colors, meshNor, this._normals, idx, i, options, modelShape); let sp = null; if (this._updatable) { sp = this._addParticle(this.nbParticles, this._lastParticleId, currentPos, currentInd, modelShape, this._shapeCounter, i, bbInfo, storage); sp.position.copyFrom(currentCopy.position); sp.rotation.copyFrom(currentCopy.rotation); if (currentCopy.rotationQuaternion) { if (sp.rotationQuaternion) { sp.rotationQuaternion.copyFrom(currentCopy.rotationQuaternion); } else { sp.rotationQuaternion = currentCopy.rotationQuaternion.clone(); } } if (currentCopy.color) { if (sp.color) { sp.color.copyFrom(currentCopy.color); } else { sp.color = currentCopy.color.clone(); } } sp.scaling.copyFrom(currentCopy.scaling); sp.uvs.copyFrom(currentCopy.uvs); if (currentCopy.materialIndex !== null) { sp.materialIndex = currentCopy.materialIndex; } if (this.expandable) { this._idxOfId[sp.id] = sp.idx; } } if (!storage) { this._index += shape.length; this.nbParticles++; this._lastParticleId++; } return sp; } /** * Sets all the particles : this method actually really updates the mesh according to the particle positions, rotations, colors, textures, etc. * This method calls `updateParticle()` for each particle of the SPS. * For an animated SPS, it is usually called within the render loop. * This methods does nothing if called on a non updatable or not yet built SPS. Example : buildMesh() not called after having added or removed particles from an expandable SPS. * @param start The particle index in the particle array where to start to compute the particle property values _(default 0)_ * @param end The particle index in the particle array where to stop to compute the particle property values _(default nbParticle - 1)_ * @param update If the mesh must be finally updated on this call after all the particle computations _(default true)_ * @returns the SPS. */ setParticles(start = 0, end = this.nbParticles - 1, update = true) { if (!this._updatable || this._isNotBuilt) { return this; } // custom beforeUpdate this.beforeUpdateParticles(start, end, update); const rotMatrix = TmpVectors.Matrix[0]; const invertedMatrix = TmpVectors.Matrix[1]; const mesh = this.mesh; const colors32 = this._colors32; const positions32 = this._positions32; const normals32 = this._normals32; const uvs32 = this._uvs32; const indices32 = this._indices32; const indices = this._indices; const fixedNormal32 = this._fixedNormal32; const depthSortParticles = this._depthSort && this._depthSortParticles; const tempVectors = TmpVectors.Vector3; const camAxisX = tempVectors[5].copyFromFloats(1.0, 0.0, 0.0); const camAxisY = tempVectors[6].copyFromFloats(0.0, 1.0, 0.0); const camAxisZ = tempVectors[7].copyFromFloats(0.0, 0.0, 1.0); const minimum = tempVectors[8].setAll(Number.MAX_VALUE); const maximum = tempVectors[9].setAll(-Number.MAX_VALUE); const camInvertedPosition = tempVectors[10].setAll(0); const tmpVertex = this._tmpVertex; const tmpVector = tmpVertex.position; const tmpColor = tmpVertex.color; const tmpUV = tmpVertex.uv; // cases when the World Matrix is to be computed first if (this.billboard || this._depthSort) { this.mesh.computeWorldMatrix(true); this.mesh._worldMatrix.invertToRef(invertedMatrix); } // if the particles will always face the camera if (this.billboard) { // compute the camera position and un-rotate it by the current mesh rotation const tmpVector0 = tempVectors[0]; this._camera.getDirectionToRef(Axis.Z, tmpVector0); Vector3.TransformNormalToRef(tmpVector0, invertedMatrix, camAxisZ); camAxisZ.normalize(); // same for camera up vector extracted from the cam view matrix const view = this._camera.getViewMatrix(true); Vector3.TransformNormalFromFloatsToRef(view.m[1], view.m[5], view.m[9], invertedMatrix, camAxisY); Vector3.CrossToRef(camAxisY, camAxisZ, camAxisX); camAxisY.normalize(); camAxisX.normalize(); } // if depthSort, compute the camera global position in the mesh local system if (this._depthSort) { Vector3.TransformCoordinatesToRef(this._camera.globalPosition, invertedMatrix, camInvertedPosition); // then un-rotate the camera } Matrix.IdentityToRef(rotMatrix); let idx = 0; // current position index in the global array positions32 let index = 0; // position start index in the global array positions32 of the current particle let colidx = 0; // current color index in the global array colors32 let colorIndex = 0; // color start index in the global array colors32 of the current particle let uvidx = 0; // current uv index in the global array uvs32 let uvIndex = 0; // uv start index in the global array uvs32 of the current particle let pt = 0; // current index in the particle model shape if (this.mesh.isFacetDataEnabled) { this._computeBoundingBox = true; } end = end >= this.nbParticles ? this.nbParticles - 1 : end; if (this._computeBoundingBox) { if (start != 0 || end != this.nbParticles - 1) { // only some particles are updated, then use the current existing BBox basis. Note : it can only increase. const boundingInfo = this.mesh.getBoundingInfo(); if (boundingInfo) { minimum.copyFrom(boundingInfo.minimum); maximum.copyFrom(boundingInfo.maximum); } } } // particle loop index = this.particles[start]._pos; const vpos = (index / 3) | 0; colorIndex = vpos * 4; uvIndex = vpos * 2; for (let p = start; p <= end; p++) { const particle = this.particles[p]; // call to custom user function to update the particle properties this.updateParticle(particle); const shape = particle._model._shape; const shapeUV = particle._model._shapeUV; const particleRotationMatrix = particle._rotationMatrix; const particlePosition = particle.position; const particleRotation = particle.rotation; const particleScaling = particle.scaling; const particleGlobalPosition = particle._globalPosition; // camera-particle distance for depth sorting if (depthSortParticles) { const dsp = this.depthSortedParticles[p]; dsp.idx = particle.idx; dsp.ind = particle._ind; dsp.indicesLength = particle._model._indicesLength; dsp.sqDistance = Vector3.DistanceSquared(particle.position, camInvertedPosition); } // skip the computations for inactive or already invisible particles if (!particle.alive || (particle._stillInvisible && !particle.isVisible && !this._recomputeInvisibles)) { // increment indexes for the next particle pt = shape.length; index += pt * 3; colorIndex += pt * 4; uvIndex += pt * 2; continue; } if (particle.isVisible) { particle._stillInvisible = false; // un-mark permanent invisibility const scaledPivot = tempVectors[12]; particle.pivot.multiplyToRef(particleScaling, scaledPivot); // particle rotation matrix if (this.billboard) { particleRotation.x = 0.0; particleRotation.y = 0.0; } if (this._computeParticleRotation || this.billboard) { particle.getRotationMatrix(rotMatrix); } const particleHasParent = particle.parentId !== null; if (particleHasParent) { const parent = this.getParticleById(particle.parentId); if (parent) { const parentRotationMatrix = parent._rotationMatrix; const parentGlobalPosition = parent._globalPosition; const rotatedY = particlePosition.x * parentRotationMatrix[1] + particlePosition.y * parentRotationMatrix[4] + particlePosition.z * parentRotationMatrix[7]; const rotatedX = particlePosition.x * parentRotationMatrix[0] + particlePosition.y * parentRotationMatrix[3] + particlePosition.z * parentRotationMatrix[6]; const rotatedZ = particlePosition.x * parentRotationMatrix[2] + particlePosition.y * parentRotationMatrix[5] + particlePosition.z * parentRotationMatrix[8]; particleGlobalPosition.x = parentGlobalPosition.x + rotatedX; particleGlobalPosition.y = parentGlobalPosition.y + rotatedY; particleGlobalPosition.z = parentGlobalPosition.z + rotatedZ; if (this._computeParticleRotation || this.billboard) { const rotMatrixValues = rotMatrix.m; particleRotationMatrix[0] = rotMatrixValues[0] * parentRotationMatrix[0] + rotMatrixValues[1] * parentRotationMatrix[3] + rotMatrixValues[2] * parentRotationMatrix[6]; particleRotationMatrix[1] = rotMatrixValues[0] * parentRotationMatrix[1] + rotMatrixValues[1] * parentRotationMatrix[4] + rotMatrixValues[2] * parentRotationMatrix[7]; particleRotationMatrix[2] = rotMatrixValues[0] * parentRotationMatrix[2] + rotMatrixValues[1] * parentRotationM