playcanvas
Version:
PlayCanvas WebGL game engine
1,106 lines (1,103 loc) • 49.5 kB
JavaScript
import { Debug } from '../../core/debug.js';
import { now } from '../../core/time.js';
import { Curve } from '../../core/math/curve.js';
import { CurveSet } from '../../core/math/curve-set.js';
import { Mat4 } from '../../core/math/mat4.js';
import { math } from '../../core/math/math.js';
import { Quat } from '../../core/math/quat.js';
import { Vec3 } from '../../core/math/vec3.js';
import { BoundingBox } from '../../core/shape/bounding-box.js';
import { FILTER_LINEAR, PRIMITIVE_TRIANGLES, requiresManualGamma, CULLFACE_NONE, SEMANTIC_ATTR0, TYPE_FLOAT32, SEMANTIC_ATTR1, SEMANTIC_ATTR2, SEMANTIC_ATTR3, SEMANTIC_ATTR4, BUFFER_DYNAMIC, INDEXFORMAT_UINT32, SEMANTIC_TEXCOORD0, typedArrayIndexFormats, ADDRESS_CLAMP_TO_EDGE, PIXELFORMAT_RGBA8, PIXELFORMAT_SRGBA8, PIXELFORMAT_RGBA32F, FILTER_NEAREST } from '../../platform/graphics/constants.js';
import { DeviceCache } from '../../platform/graphics/device-cache.js';
import { IndexBuffer } from '../../platform/graphics/index-buffer.js';
import { RenderTarget } from '../../platform/graphics/render-target.js';
import { Texture } from '../../platform/graphics/texture.js';
import { VertexBuffer } from '../../platform/graphics/vertex-buffer.js';
import { VertexFormat } from '../../platform/graphics/vertex-format.js';
import { EMITTERSHAPE_BOX, PARTICLESORT_NONE, PARTICLEORIENTATION_SCREEN, PARTICLEORIENTATION_WORLD, PARTICLEMODE_GPU, BLEND_NORMAL } from '../constants.js';
import { Mesh } from '../mesh.js';
import { MeshInstance } from '../mesh-instance.js';
import { createShaderFromCode } from '../shader-lib/utils.js';
import { shaderChunks } from '../shader-lib/chunks/chunks.js';
import { ParticleCPUUpdater } from './cpu-updater.js';
import { ParticleGPUUpdater } from './gpu-updater.js';
import { ParticleMaterial } from './particle-material.js';
var particleVerts = [
[
-1,
-1
],
[
1,
-1
],
[
1,
1
],
[
-1,
1
]
];
function _createTexture(device, width, height, pixelData, format, mult8Bit, filter) {
if (format === undefined) format = PIXELFORMAT_RGBA32F;
var mipFilter = FILTER_NEAREST;
if (filter && (format === PIXELFORMAT_RGBA8 || format === PIXELFORMAT_SRGBA8)) {
mipFilter = FILTER_LINEAR;
}
var texture = new Texture(device, {
width: width,
height: height,
format: format,
cubemap: false,
mipmaps: false,
minFilter: mipFilter,
magFilter: mipFilter,
addressU: ADDRESS_CLAMP_TO_EDGE,
addressV: ADDRESS_CLAMP_TO_EDGE,
name: 'ParticleSystemTexture'
});
var pixels = texture.lock();
if (format === PIXELFORMAT_RGBA8 || format === PIXELFORMAT_SRGBA8) {
var temp = new Uint8Array(pixelData.length);
for(var i = 0; i < pixelData.length; i++){
temp[i] = pixelData[i] * mult8Bit * 255;
}
pixelData = temp;
}
pixels.set(pixelData);
texture.unlock();
return texture;
}
function saturate(x) {
return Math.max(Math.min(x, 1), 0);
}
var default0Curve = new Curve([
0,
0,
1,
0
]);
var default1Curve = new Curve([
0,
1,
1,
1
]);
var default0Curve3 = new CurveSet([
0,
0,
1,
0
], [
0,
0,
1,
0
], [
0,
0,
1,
0
]);
var default1Curve3 = new CurveSet([
0,
1,
1,
1
], [
0,
1,
1,
1
], [
0,
1,
1,
1
]);
var particleTexHeight = 2;
var particleTexChannels = 4; // there is a duplicate in cpu updater
var extentsInnerRatioUniform = new Float32Array(3);
var spawnMatrix = new Mat4();
var tmpVec3 = new Vec3();
var bMin = new Vec3();
var bMax = new Vec3();
var setPropertyTarget;
var setPropertyOptions;
function setProperty(pName, defaultVal) {
if (setPropertyOptions[pName] !== undefined && setPropertyOptions[pName] !== null) {
setPropertyTarget[pName] = setPropertyOptions[pName];
} else {
setPropertyTarget[pName] = defaultVal;
}
}
function pack3NFloats(a, b, c) {
var packed = a * 255 << 16 | b * 255 << 8 | c * 255;
return packed / (1 << 24);
}
function packTextureXYZ_NXYZ(qXYZ, qXYZ2) {
var num = qXYZ.length / 3;
var colors = new Array(num * 4);
for(var i = 0; i < num; i++){
colors[i * 4] = qXYZ[i * 3];
colors[i * 4 + 1] = qXYZ[i * 3 + 1];
colors[i * 4 + 2] = qXYZ[i * 3 + 2];
colors[i * 4 + 3] = pack3NFloats(qXYZ2[i * 3], qXYZ2[i * 3 + 1], qXYZ2[i * 3 + 2]);
}
return colors;
}
function packTextureRGBA(qRGB, qA) {
var colors = new Array(qA.length * 4);
for(var i = 0; i < qA.length; i++){
colors[i * 4] = qRGB[i * 3];
colors[i * 4 + 1] = qRGB[i * 3 + 1];
colors[i * 4 + 2] = qRGB[i * 3 + 2];
colors[i * 4 + 3] = qA[i];
}
return colors;
}
function packTexture5Floats(qA, qB, qC, qD, qE) {
var colors = new Array(qA.length * 4);
for(var i = 0; i < qA.length; i++){
colors[i * 4] = qA[i];
colors[i * 4 + 1] = qB[i];
colors[i * 4 + 2] = 0;
colors[i * 4 + 3] = pack3NFloats(qC[i], qD[i], qE[i]);
}
return colors;
}
function packTexture2Floats(qA, qB) {
var colors = new Array(qA.length * 4);
for(var i = 0; i < qA.length; i++){
colors[i * 4] = qA[i];
colors[i * 4 + 1] = qB[i];
colors[i * 4 + 2] = 0;
colors[i * 4 + 3] = 0;
}
return colors;
}
function calcEndTime(emitter) {
var interval = Math.max(emitter.rate, emitter.rate2) * emitter.numParticles + emitter.lifetime;
return Date.now() + interval * 1000;
}
function subGraph(A, B) {
var r = new Float32Array(A.length);
for(var i = 0; i < A.length; i++){
r[i] = A[i] - B[i];
}
return r;
}
function maxUnsignedGraphValue(A, outUMax) {
var chans = outUMax.length;
var values = A.length / chans;
for(var i = 0; i < values; i++){
for(var j = 0; j < chans; j++){
var a = Math.abs(A[i * chans + j]);
outUMax[j] = Math.max(outUMax[j], a);
}
}
}
function normalizeGraph(A, uMax) {
var chans = uMax.length;
var values = A.length / chans;
for(var i = 0; i < values; i++){
for(var j = 0; j < chans; j++){
A[i * chans + j] /= uMax[j] === 0 ? 1 : uMax[j];
A[i * chans + j] *= 0.5;
A[i * chans + j] += 0.5;
}
}
}
function divGraphFrom2Curves(curve1, curve2, outUMax) {
var sub = subGraph(curve2, curve1);
maxUnsignedGraphValue(sub, outUMax);
normalizeGraph(sub, outUMax);
return sub;
}
// a device cache storing default parameter texture for the emitter
var particleEmitterDeviceCache = new DeviceCache();
class ParticleEmitter {
get defaultParamTexture() {
Debug.assert(this.graphicsDevice);
return particleEmitterDeviceCache.get(this.graphicsDevice, ()=>{
var resolution = 16;
var centerPoint = resolution * 0.5 + 0.5;
var dtex = new Float32Array(resolution * resolution * 4);
for(var y = 0; y < resolution; y++){
for(var x = 0; x < resolution; x++){
var xgrad = x + 1 - centerPoint;
var ygrad = y + 1 - centerPoint;
var c = saturate(1 - saturate(Math.sqrt(xgrad * xgrad + ygrad * ygrad) / resolution) - 0.5);
var p = y * resolution + x;
dtex[p * 4] = 1;
dtex[p * 4 + 1] = 1;
dtex[p * 4 + 2] = 1;
dtex[p * 4 + 3] = c;
}
}
var texture = _createTexture(this.graphicsDevice, resolution, resolution, dtex, PIXELFORMAT_SRGBA8, 1.0, true);
texture.minFilter = FILTER_LINEAR;
texture.magFilter = FILTER_LINEAR;
return texture;
});
}
onChangeCamera() {
this.resetMaterial();
}
calculateBoundsMad() {
this.worldBoundsMul.x = 1.0 / this.worldBoundsSize.x;
this.worldBoundsMul.y = 1.0 / this.worldBoundsSize.y;
this.worldBoundsMul.z = 1.0 / this.worldBoundsSize.z;
this.worldBoundsAdd.copy(this.worldBounds.center).mul(this.worldBoundsMul).mulScalar(-1);
this.worldBoundsAdd.x += 0.5;
this.worldBoundsAdd.y += 0.5;
this.worldBoundsAdd.z += 0.5;
}
calculateWorldBounds() {
if (!this.node) return;
this.prevWorldBoundsSize.copy(this.worldBoundsSize);
this.prevWorldBoundsCenter.copy(this.worldBounds.center);
if (!this.useCpu) {
var recalculateLocalBounds = false;
if (this.emitterShape === EMITTERSHAPE_BOX) {
recalculateLocalBounds = !this.emitterExtents.equals(this.prevEmitterExtents);
} else {
recalculateLocalBounds = !(this.emitterRadius === this.prevEmitterRadius);
}
if (recalculateLocalBounds) {
this.calculateLocalBounds();
}
}
var nodeWT = this.node.getWorldTransform();
if (this.localSpace) {
this.worldBoundsNoTrail.copy(this.localBounds);
} else {
this.worldBoundsNoTrail.setFromTransformedAabb(this.localBounds, nodeWT);
}
this.worldBoundsTrail[0].add(this.worldBoundsNoTrail);
this.worldBoundsTrail[1].add(this.worldBoundsNoTrail);
var now = this.simTimeTotal;
if (now >= this.timeToSwitchBounds) {
this.worldBoundsTrail[0].copy(this.worldBoundsTrail[1]);
this.worldBoundsTrail[1].copy(this.worldBoundsNoTrail);
this.timeToSwitchBounds = now + this.lifetime;
}
this.worldBounds.copy(this.worldBoundsTrail[0]);
this.worldBoundsSize.copy(this.worldBounds.halfExtents).mulScalar(2);
if (this.localSpace) {
this.meshInstance.aabb.setFromTransformedAabb(this.worldBounds, nodeWT);
this.meshInstance.mesh.aabb.setFromTransformedAabb(this.worldBounds, nodeWT);
} else {
this.meshInstance.aabb.copy(this.worldBounds);
this.meshInstance.mesh.aabb.copy(this.worldBounds);
}
this.meshInstance._aabbVer = 1 - this.meshInstance._aabbVer;
if (this.pack8) this.calculateBoundsMad();
}
resetWorldBounds() {
if (!this.node) return;
this.worldBoundsNoTrail.setFromTransformedAabb(this.localBounds, this.localSpace ? Mat4.IDENTITY : this.node.getWorldTransform());
this.worldBoundsTrail[0].copy(this.worldBoundsNoTrail);
this.worldBoundsTrail[1].copy(this.worldBoundsNoTrail);
this.worldBounds.copy(this.worldBoundsTrail[0]);
this.worldBoundsSize.copy(this.worldBounds.halfExtents).mulScalar(2);
this.prevWorldBoundsSize.copy(this.worldBoundsSize);
this.prevWorldBoundsCenter.copy(this.worldBounds.center);
this.simTimeTotal = 0;
this.timeToSwitchBounds = 0;
}
calculateLocalBounds() {
var minx = Number.MAX_VALUE;
var miny = Number.MAX_VALUE;
var minz = Number.MAX_VALUE;
var maxx = -Number.MAX_VALUE;
var maxy = -Number.MAX_VALUE;
var maxz = -Number.MAX_VALUE;
var maxR = 0;
var maxScale = 0;
var stepWeight = this.lifetime / this.precision;
var wVels = [
this.qVelocity,
this.qVelocity2
];
var lVels = [
this.qLocalVelocity,
this.qLocalVelocity2
];
var accumX = [
0,
0
];
var accumY = [
0,
0
];
var accumZ = [
0,
0
];
var accumR = [
0,
0
];
var accumW = [
0,
0
];
var x, y, z;
for(var i = 0; i < this.precision + 1; i++){
var index = Math.min(i, this.precision - 1);
for(var j = 0; j < 2; j++){
x = lVels[j][index * 3 + 0] * stepWeight + accumX[j];
y = lVels[j][index * 3 + 1] * stepWeight + accumY[j];
z = lVels[j][index * 3 + 2] * stepWeight + accumZ[j];
minx = Math.min(x, minx);
miny = Math.min(y, miny);
minz = Math.min(z, minz);
maxx = Math.max(x, maxx);
maxy = Math.max(y, maxy);
maxz = Math.max(z, maxz);
accumX[j] = x;
accumY[j] = y;
accumZ[j] = z;
}
for(var j1 = 0; j1 < 2; j1++){
accumW[j1] += stepWeight * Math.sqrt(wVels[j1][index * 3 + 0] * wVels[j1][index * 3 + 0] + wVels[j1][index * 3 + 1] * wVels[j1][index * 3 + 1] + wVels[j1][index * 3 + 2] * wVels[j1][index * 3 + 2]);
}
accumR[0] += this.qRadialSpeed[index] * stepWeight;
accumR[1] += this.qRadialSpeed2[index] * stepWeight;
maxR = Math.max(maxR, Math.max(Math.abs(accumR[0]), Math.abs(accumR[1])));
maxScale = Math.max(maxScale, this.qScale[index]);
}
if (this.emitterShape === EMITTERSHAPE_BOX) {
x = this.emitterExtents.x * 0.5;
y = this.emitterExtents.y * 0.5;
z = this.emitterExtents.z * 0.5;
} else {
x = this.emitterRadius;
y = this.emitterRadius;
z = this.emitterRadius;
}
var w = Math.max(accumW[0], accumW[1]);
bMin.x = minx - maxScale - x - maxR - w;
bMin.y = miny - maxScale - y - maxR - w;
bMin.z = minz - maxScale - z - maxR - w;
bMax.x = maxx + maxScale + x + maxR + w;
bMax.y = maxy + maxScale + y + maxR + w;
bMax.z = maxz + maxScale + z + maxR + w;
this.localBounds.setMinMax(bMin, bMax);
}
rebuild() {
var gd = this.graphicsDevice;
if (this.colorMap === null) this.colorMap = this.defaultParamTexture;
this.spawnBounds = this.emitterShape === EMITTERSHAPE_BOX ? this.emitterExtents : this.emitterRadius;
this.useCpu = this.useCpu || this.sort > PARTICLESORT_NONE || // force CPU if desirable by user or sorting is enabled
gd.maxVertexTextures <= 1 || // force CPU if can't use enough vertex textures
gd.fragmentUniformsCount < 64 || // force CPU if can't use many uniforms; TODO: change to more realistic value (this one is iphone's)
gd.forceCpuParticles;
this._destroyResources();
this.pack8 = (this.pack8 || !gd.textureFloatRenderable) && !this.useCpu;
particleTexHeight = this.useCpu || this.pack8 ? 4 : 2;
this.useMesh = !!this.mesh;
this.numParticlesPot = math.nextPowerOfTwo(this.numParticles);
this.rebuildGraphs();
this.calculateLocalBounds();
this.resetWorldBounds();
if (this.node) {
// this.prevPos.copy(this.node.getPosition());
this.worldBounds.setFromTransformedAabb(this.localBounds, this.localSpace ? Mat4.IDENTITY : this.node.getWorldTransform());
this.worldBoundsTrail[0].copy(this.worldBounds);
this.worldBoundsTrail[1].copy(this.worldBounds);
this.worldBoundsSize.copy(this.worldBounds.halfExtents).mulScalar(2);
this.prevWorldBoundsSize.copy(this.worldBoundsSize);
this.prevWorldBoundsCenter.copy(this.worldBounds.center);
if (this.pack8) this.calculateBoundsMad();
}
// Dynamic simulation data
this.vbToSort = new Array(this.numParticles);
for(var iSort = 0; iSort < this.numParticles; iSort++)this.vbToSort[iSort] = [
0,
0
];
this.particleDistance = new Float32Array(this.numParticles);
this._gpuUpdater.randomize();
this.particleTex = new Float32Array(this.numParticlesPot * particleTexHeight * particleTexChannels);
var emitterPos = this.node === null || this.localSpace ? Vec3.ZERO : this.node.getPosition();
if (this.emitterShape === EMITTERSHAPE_BOX) {
if (this.node === null || this.localSpace) {
spawnMatrix.setTRS(Vec3.ZERO, Quat.IDENTITY, this.spawnBounds);
} else {
spawnMatrix.setTRS(Vec3.ZERO, this.node.getRotation(), tmpVec3.copy(this.spawnBounds).mul(this.node.localScale));
}
extentsInnerRatioUniform[0] = this.emitterExtents.x !== 0 ? this.emitterExtentsInner.x / this.emitterExtents.x : 0;
extentsInnerRatioUniform[1] = this.emitterExtents.y !== 0 ? this.emitterExtentsInner.y / this.emitterExtents.y : 0;
extentsInnerRatioUniform[2] = this.emitterExtents.z !== 0 ? this.emitterExtentsInner.z / this.emitterExtents.z : 0;
}
for(var i = 0; i < this.numParticles; i++){
this._cpuUpdater.calcSpawnPosition(this.particleTex, spawnMatrix, extentsInnerRatioUniform, emitterPos, i);
if (this.useCpu) this.particleTex[i * particleTexChannels + 3 + this.numParticlesPot * 2 * particleTexChannels] = 1; // hide/show
}
this.particleTexStart = new Float32Array(this.numParticlesPot * particleTexHeight * particleTexChannels);
for(var i1 = 0; i1 < this.particleTexStart.length; i1++){
this.particleTexStart[i1] = this.particleTex[i1];
}
if (!this.useCpu) {
if (this.pack8) {
this.particleTexIN = _createTexture(gd, this.numParticlesPot, particleTexHeight, this.particleTex, PIXELFORMAT_RGBA8, 1, false);
this.particleTexOUT = _createTexture(gd, this.numParticlesPot, particleTexHeight, this.particleTex, PIXELFORMAT_RGBA8, 1, false);
this.particleTexStart = _createTexture(gd, this.numParticlesPot, particleTexHeight, this.particleTexStart, PIXELFORMAT_RGBA8, 1, false);
} else {
this.particleTexIN = _createTexture(gd, this.numParticlesPot, particleTexHeight, this.particleTex);
this.particleTexOUT = _createTexture(gd, this.numParticlesPot, particleTexHeight, this.particleTex);
this.particleTexStart = _createTexture(gd, this.numParticlesPot, particleTexHeight, this.particleTexStart);
}
this.rtParticleTexIN = new RenderTarget({
colorBuffer: this.particleTexIN,
depth: false
});
this.rtParticleTexOUT = new RenderTarget({
colorBuffer: this.particleTexOUT,
depth: false
});
this.swapTex = false;
}
var shaderCodeStart = (this.localSpace ? '#define LOCAL_SPACE\n' : '') + shaderChunks.particleUpdaterInitPS + (this.pack8 ? shaderChunks.particleInputRgba8PS + shaderChunks.particleOutputRgba8PS : shaderChunks.particleInputFloatPS + shaderChunks.particleOutputFloatPS) + (this.emitterShape === EMITTERSHAPE_BOX ? shaderChunks.particleUpdaterAABBPS : shaderChunks.particleUpdaterSpherePS) + shaderChunks.particleUpdaterStartPS;
var shaderCodeRespawn = shaderCodeStart + shaderChunks.particleUpdaterRespawnPS + shaderChunks.particleUpdaterEndPS;
var shaderCodeNoRespawn = shaderCodeStart + shaderChunks.particleUpdaterNoRespawnPS + shaderChunks.particleUpdaterEndPS;
var shaderCodeOnStop = shaderCodeStart + shaderChunks.particleUpdaterOnStopPS + shaderChunks.particleUpdaterEndPS;
// Note: createShaderFromCode can return a shader from the cache (not a new shader) so we *should not* delete these shaders
// when the particle emitter is destroyed
var params = "Shape:" + this.emitterShape + "-Pack:" + this.pack8 + "-Local:" + this.localSpace;
this.shaderParticleUpdateRespawn = createShaderFromCode(gd, shaderChunks.fullscreenQuadVS, shaderCodeRespawn, "ParticleUpdateRespawn-" + params);
this.shaderParticleUpdateNoRespawn = createShaderFromCode(gd, shaderChunks.fullscreenQuadVS, shaderCodeNoRespawn, "ParticleUpdateNoRespawn-" + params);
this.shaderParticleUpdateOnStop = createShaderFromCode(gd, shaderChunks.fullscreenQuadVS, shaderCodeOnStop, "ParticleUpdateStop-" + params);
this.numParticleVerts = this.useMesh ? this.mesh.vertexBuffer.numVertices : 4;
this.numParticleIndices = this.useMesh ? this.mesh.indexBuffer[0].numIndices : 6;
this._allocate(this.numParticles);
var mesh = new Mesh(gd);
mesh.vertexBuffer = this.vertexBuffer;
mesh.indexBuffer[0] = this.indexBuffer;
mesh.primitive[0].type = PRIMITIVE_TRIANGLES;
mesh.primitive[0].base = 0;
mesh.primitive[0].count = this.numParticles * this.numParticleIndices;
mesh.primitive[0].indexed = true;
this.material = this._createMaterial();
this.resetMaterial();
var wasVisible = this.meshInstance ? this.meshInstance.visible : true;
this.meshInstance = new MeshInstance(mesh, this.material, this.node);
this.meshInstance.pick = false;
this.meshInstance.updateKey(); // shouldn't be here?
this.meshInstance.cull = true;
if (this.localSpace) {
this.meshInstance.aabb.setFromTransformedAabb(this.worldBounds, this.node.getWorldTransform());
} else {
this.meshInstance.aabb.copy(this.worldBounds);
}
this.meshInstance._updateAabb = false;
this.meshInstance.visible = wasVisible;
this._setMaterialTextures();
this.resetTime();
this.addTime(0, false); // fill dynamic textures and constants with initial data
if (this.preWarm) this.prewarm(this.lifetime);
}
_isAnimated() {
return this.animNumFrames >= 1 && (this.animTilesX > 1 || this.animTilesY > 1) && (this.colorMap && this.colorMap !== this.defaultParamTexture || this.normalMap);
}
rebuildGraphs() {
var precision = this.precision;
var gd = this.graphicsDevice;
this.qLocalVelocity = this.localVelocityGraph.quantize(precision);
this.qVelocity = this.velocityGraph.quantize(precision);
this.qColor = this.colorGraph.quantizeClamped(precision, 0, 1);
this.qRotSpeed = this.rotationSpeedGraph.quantize(precision);
this.qScale = this.scaleGraph.quantize(precision);
this.qAlpha = this.alphaGraph.quantize(precision);
this.qRadialSpeed = this.radialSpeedGraph.quantize(precision);
this.qLocalVelocity2 = this.localVelocityGraph2.quantize(precision);
this.qVelocity2 = this.velocityGraph2.quantize(precision);
this.qColor2 = this.colorGraph2.quantizeClamped(precision, 0, 1);
this.qRotSpeed2 = this.rotationSpeedGraph2.quantize(precision);
this.qScale2 = this.scaleGraph2.quantize(precision);
this.qAlpha2 = this.alphaGraph2.quantize(precision);
this.qRadialSpeed2 = this.radialSpeedGraph2.quantize(precision);
for(var i = 0; i < precision; i++){
this.qRotSpeed[i] *= math.DEG_TO_RAD;
this.qRotSpeed2[i] *= math.DEG_TO_RAD;
}
this.localVelocityUMax = new Float32Array(3);
this.velocityUMax = new Float32Array(3);
this.colorUMax = new Float32Array(3);
this.rotSpeedUMax = [
0
];
this.scaleUMax = [
0
];
this.alphaUMax = [
0
];
this.radialSpeedUMax = [
0
];
this.qLocalVelocityDiv = divGraphFrom2Curves(this.qLocalVelocity, this.qLocalVelocity2, this.localVelocityUMax);
this.qVelocityDiv = divGraphFrom2Curves(this.qVelocity, this.qVelocity2, this.velocityUMax);
this.qColorDiv = divGraphFrom2Curves(this.qColor, this.qColor2, this.colorUMax);
this.qRotSpeedDiv = divGraphFrom2Curves(this.qRotSpeed, this.qRotSpeed2, this.rotSpeedUMax);
this.qScaleDiv = divGraphFrom2Curves(this.qScale, this.qScale2, this.scaleUMax);
this.qAlphaDiv = divGraphFrom2Curves(this.qAlpha, this.qAlpha2, this.alphaUMax);
this.qRadialSpeedDiv = divGraphFrom2Curves(this.qRadialSpeed, this.qRadialSpeed2, this.radialSpeedUMax);
if (this.pack8) {
var umax = [
0,
0,
0
];
maxUnsignedGraphValue(this.qVelocity, umax);
var umax2 = [
0,
0,
0
];
maxUnsignedGraphValue(this.qVelocity2, umax2);
var lumax = [
0,
0,
0
];
maxUnsignedGraphValue(this.qLocalVelocity, lumax);
var lumax2 = [
0,
0,
0
];
maxUnsignedGraphValue(this.qLocalVelocity2, lumax2);
var rumax = [
0
];
maxUnsignedGraphValue(this.qRadialSpeed, rumax);
var rumax2 = [
0
];
maxUnsignedGraphValue(this.qRadialSpeed2, rumax2);
var maxVel = Math.max(umax[0], umax2[0]);
maxVel = Math.max(maxVel, umax[1]);
maxVel = Math.max(maxVel, umax2[1]);
maxVel = Math.max(maxVel, umax[2]);
maxVel = Math.max(maxVel, umax2[2]);
var lmaxVel = Math.max(lumax[0], lumax2[0]);
lmaxVel = Math.max(lmaxVel, lumax[1]);
lmaxVel = Math.max(lmaxVel, lumax2[1]);
lmaxVel = Math.max(lmaxVel, lumax[2]);
lmaxVel = Math.max(lmaxVel, lumax2[2]);
var maxRad = Math.max(rumax[0], rumax2[0]);
this.maxVel = maxVel + lmaxVel + maxRad;
}
if (!this.useCpu) {
this.internalTex0 = _createTexture(gd, precision, 1, packTextureXYZ_NXYZ(this.qLocalVelocity, this.qLocalVelocityDiv));
this.internalTex1 = _createTexture(gd, precision, 1, packTextureXYZ_NXYZ(this.qVelocity, this.qVelocityDiv));
this.internalTex2 = _createTexture(gd, precision, 1, packTexture5Floats(this.qRotSpeed, this.qScale, this.qScaleDiv, this.qRotSpeedDiv, this.qAlphaDiv));
this.internalTex3 = _createTexture(gd, precision, 1, packTexture2Floats(this.qRadialSpeed, this.qRadialSpeedDiv));
}
this.colorParam = _createTexture(gd, precision, 1, packTextureRGBA(this.qColor, this.qAlpha), PIXELFORMAT_SRGBA8, 1.0, true);
}
_setMaterialTextures() {
if (this.colorMap) {
Debug.call(()=>{
if (requiresManualGamma(this.colorMap.format)) {
Debug.warnOnce("ParticleEmitter: colorMap texture [" + this.colorMap.name + "] is not using sRGB format. Please correct it for the correct rendering.", this.colorMap);
}
});
this.material.setParameter('colorMap', this.colorMap);
if (this.lighting && this.normalMap) {
this.material.setParameter('normalMap', this.normalMap);
}
}
}
_createMaterial() {
var material = new ParticleMaterial(this);
material.name = "EmitterMaterial:" + this.node.name;
material.cull = CULLFACE_NONE;
material.alphaWrite = false;
material.blendType = this.blendType;
material.depthWrite = this.depthWrite;
return material;
}
resetMaterial() {
var material = this.material;
material.setParameter('stretch', this.stretch);
if (this._isAnimated()) {
material.setParameter('animTexTilesParams', this.animTilesParams);
material.setParameter('animTexParams', this.animParams);
material.setParameter('animTexIndexParams', this.animIndexParams);
}
material.setParameter('colorMult', this.intensity);
if (!this.useCpu) {
material.setParameter('internalTex0', this.internalTex0);
material.setParameter('internalTex1', this.internalTex1);
material.setParameter('internalTex2', this.internalTex2);
material.setParameter('internalTex3', this.internalTex3);
}
material.setParameter('colorParam', this.colorParam);
material.setParameter('numParticles', this.numParticles);
material.setParameter('numParticlesPot', this.numParticlesPot);
material.setParameter('lifetime', this.lifetime);
material.setParameter('rate', this.rate);
material.setParameter('rateDiv', this.rate2 - this.rate);
material.setParameter('seed', this.seed);
material.setParameter('scaleDivMult', this.scaleUMax[0]);
material.setParameter('alphaDivMult', this.alphaUMax[0]);
material.setParameter('radialSpeedDivMult', this.radialSpeedUMax[0]);
material.setParameter('graphNumSamples', this.precision);
material.setParameter('graphSampleSize', 1.0 / this.precision);
material.setParameter('emitterScale', new Float32Array([
1,
1,
1
]));
if (this.pack8) {
this._gpuUpdater._setInputBounds();
material.setParameter('inBoundsSize', this._gpuUpdater.inBoundsSizeUniform);
material.setParameter('inBoundsCenter', this._gpuUpdater.inBoundsCenterUniform);
material.setParameter('maxVel', this.maxVel);
}
if (this.wrap && this.wrapBounds) {
this.wrapBoundsUniform[0] = this.wrapBounds.x;
this.wrapBoundsUniform[1] = this.wrapBounds.y;
this.wrapBoundsUniform[2] = this.wrapBounds.z;
material.setParameter('wrapBounds', this.wrapBoundsUniform);
}
this._setMaterialTextures();
if (this.depthSoftening > 0) {
material.setParameter('softening', 1.0 / (this.depthSoftening * this.depthSoftening * 100)); // remap to more perceptually linear
}
if (this.stretch > 0.0) material.cull = CULLFACE_NONE;
this._compParticleFaceParams();
}
_compParticleFaceParams() {
var tangent, binormal;
if (this.orientation === PARTICLEORIENTATION_SCREEN) {
tangent = new Float32Array([
1,
0,
0
]);
binormal = new Float32Array([
0,
0,
1
]);
} else {
var n;
if (this.orientation === PARTICLEORIENTATION_WORLD) {
n = this.particleNormal.normalize();
} else {
var emitterMat = this.node === null ? Mat4.IDENTITY : this.node.getWorldTransform();
n = emitterMat.transformVector(this.particleNormal).normalize();
}
var t = new Vec3(1, 0, 0);
if (Math.abs(t.dot(n)) === 1) {
t.set(0, 0, 1);
}
var b = new Vec3().cross(n, t).normalize();
t.cross(b, n).normalize();
tangent = new Float32Array([
t.x,
t.y,
t.z
]);
binormal = new Float32Array([
b.x,
b.y,
b.z
]);
}
this.material.setParameter('faceTangent', tangent);
this.material.setParameter('faceBinorm', binormal);
}
// Declares vertex format, creates VB and IB
_allocate(numParticles) {
var psysVertCount = numParticles * this.numParticleVerts;
var psysIndexCount = numParticles * this.numParticleIndices;
if (this.vertexBuffer === undefined || this.vertexBuffer.getNumVertices() !== psysVertCount) {
// Create the particle vertex format
var elements = [];
if (!this.useCpu) {
// GPU: XYZ = quad vertex position; W = INT: particle ID, FRAC: random factor
elements.push({
semantic: SEMANTIC_ATTR0,
components: 4,
type: TYPE_FLOAT32
});
if (this.useMesh) {
elements.push({
semantic: SEMANTIC_ATTR1,
components: 2,
type: TYPE_FLOAT32
});
}
} else {
elements.push({
semantic: SEMANTIC_ATTR0,
components: 4,
type: TYPE_FLOAT32
}, {
semantic: SEMANTIC_ATTR1,
components: 4,
type: TYPE_FLOAT32
}, {
semantic: SEMANTIC_ATTR2,
components: 4,
type: TYPE_FLOAT32
}, {
semantic: SEMANTIC_ATTR3,
components: 1,
type: TYPE_FLOAT32
}, {
semantic: SEMANTIC_ATTR4,
components: this.useMesh ? 4 : 2,
type: TYPE_FLOAT32
});
}
var vertexFormat = new VertexFormat(this.graphicsDevice, elements);
this.vertexBuffer = new VertexBuffer(this.graphicsDevice, vertexFormat, psysVertCount, {
usage: BUFFER_DYNAMIC
});
this.indexBuffer = new IndexBuffer(this.graphicsDevice, INDEXFORMAT_UINT32, psysIndexCount);
// Fill the vertex buffer
var data = new Float32Array(this.vertexBuffer.lock());
var meshData, stride, texCoordOffset;
if (this.useMesh) {
meshData = new Float32Array(this.mesh.vertexBuffer.lock());
stride = meshData.length / this.mesh.vertexBuffer.numVertices;
for(var elem = 0; elem < this.mesh.vertexBuffer.format.elements.length; elem++){
if (this.mesh.vertexBuffer.format.elements[elem].name === SEMANTIC_TEXCOORD0) {
texCoordOffset = this.mesh.vertexBuffer.format.elements[elem].offset / 4;
break;
}
}
}
for(var i = 0; i < psysVertCount; i++){
var id = Math.floor(i / this.numParticleVerts);
if (!this.useMesh) {
var vertID = i % 4;
data[i * 4] = particleVerts[vertID][0];
data[i * 4 + 1] = particleVerts[vertID][1];
data[i * 4 + 2] = 0;
data[i * 4 + 3] = id;
} else {
var vert = i % this.numParticleVerts;
data[i * 6] = meshData[vert * stride];
data[i * 6 + 1] = meshData[vert * stride + 1];
data[i * 6 + 2] = meshData[vert * stride + 2];
data[i * 6 + 3] = id;
data[i * 6 + 4] = meshData[vert * stride + texCoordOffset + 0];
data[i * 6 + 5] = 1.0 - meshData[vert * stride + texCoordOffset + 1];
}
}
if (this.useCpu) {
this.vbCPU = new Float32Array(data);
this.vbOld = new Float32Array(this.vbCPU.length);
}
this.vertexBuffer.unlock();
if (this.useMesh) {
this.mesh.vertexBuffer.unlock();
}
// Fill the index buffer
var dst = 0;
var indices = new Uint32Array(this.indexBuffer.lock());
if (this.useMesh) {
var ib = this.mesh.indexBuffer[0];
meshData = new typedArrayIndexFormats[ib.format](ib.lock());
}
for(var i1 = 0; i1 < numParticles; i1++){
if (!this.useMesh) {
var baseIndex = i1 * 4;
indices[dst++] = baseIndex;
indices[dst++] = baseIndex + 1;
indices[dst++] = baseIndex + 2;
indices[dst++] = baseIndex;
indices[dst++] = baseIndex + 2;
indices[dst++] = baseIndex + 3;
} else {
for(var j = 0; j < this.numParticleIndices; j++){
indices[i1 * this.numParticleIndices + j] = meshData[j] + i1 * this.numParticleVerts;
}
}
}
this.indexBuffer.unlock();
if (this.useMesh) this.mesh.indexBuffer[0].unlock();
}
}
reset() {
this.beenReset = true;
this.seed = Math.random();
this.material.setParameter('seed', this.seed);
if (this.useCpu) {
for(var i = 0; i < this.particleTexStart.length; i++){
this.particleTex[i] = this.particleTexStart[i];
}
} else {
this._setMaterialTextures();
}
this.resetWorldBounds();
this.resetTime();
var origLoop = this.loop;
this.loop = true;
this.addTime(0, false);
this.loop = origLoop;
if (this.preWarm) {
this.prewarm(this.lifetime);
}
}
prewarm(time) {
var lifetimeFraction = time / this.lifetime;
var iterations = Math.min(Math.floor(lifetimeFraction * this.precision), this.precision);
var stepDelta = time / iterations;
for(var i = 0; i < iterations; i++){
this.addTime(stepDelta, false);
}
}
resetTime() {
this.endTime = calcEndTime(this);
}
finishFrame() {
if (this.useCpu) this.vertexBuffer.unlock();
}
addTime(delta, isOnStop) {
var device = this.graphicsDevice;
var startTime = now();
this.simTimeTotal += delta;
this.calculateWorldBounds();
if (this._isAnimated()) {
var tilesParams = this.animTilesParams;
tilesParams[0] = 1.0 / this.animTilesX; // animTexTilesParams.x
tilesParams[1] = 1.0 / this.animTilesY; // animTexTilesParams.y
var params = this.animParams;
params[0] = this.animStartFrame; // animTexParams.x
params[1] = this.animNumFrames * this.animSpeed; // animTexParams.y
params[2] = this.animNumFrames - 1; // animTexParams.z
params[3] = this.animNumAnimations - 1; // animTexParams.w
var animIndexParams = this.animIndexParams;
animIndexParams[0] = this.animIndex; // animTexIndexParams.x
animIndexParams[1] = this.randomizeAnimIndex; // animTexIndexParams.y
}
if (this.scene) {
if (this.camera !== this.scene._activeCamera) {
this.camera = this.scene._activeCamera;
this.onChangeCamera();
}
}
if (this.emitterShape === EMITTERSHAPE_BOX) {
extentsInnerRatioUniform[0] = this.emitterExtents.x !== 0 ? this.emitterExtentsInner.x / this.emitterExtents.x : 0;
extentsInnerRatioUniform[1] = this.emitterExtents.y !== 0 ? this.emitterExtentsInner.y / this.emitterExtents.y : 0;
extentsInnerRatioUniform[2] = this.emitterExtents.z !== 0 ? this.emitterExtentsInner.z / this.emitterExtents.z : 0;
if (this.meshInstance.node === null) {
spawnMatrix.setTRS(Vec3.ZERO, Quat.IDENTITY, this.emitterExtents);
} else {
spawnMatrix.setTRS(Vec3.ZERO, this.meshInstance.node.getRotation(), tmpVec3.copy(this.emitterExtents).mul(this.meshInstance.node.localScale));
}
}
var emitterPos;
var emitterScale = this.meshInstance.node === null ? Vec3.ONE : this.meshInstance.node.localScale;
this.emitterScaleUniform[0] = emitterScale.x;
this.emitterScaleUniform[1] = emitterScale.y;
this.emitterScaleUniform[2] = emitterScale.z;
this.material.setParameter('emitterScale', this.emitterScaleUniform);
if (this.localSpace && this.meshInstance.node) {
emitterPos = this.meshInstance.node.getPosition();
this.emitterPosUniform[0] = emitterPos.x;
this.emitterPosUniform[1] = emitterPos.y;
this.emitterPosUniform[2] = emitterPos.z;
this.material.setParameter('emitterPos', this.emitterPosUniform);
}
this._compParticleFaceParams();
if (!this.useCpu) {
this._gpuUpdater.update(device, spawnMatrix, extentsInnerRatioUniform, delta, isOnStop);
} else {
var data = new Float32Array(this.vertexBuffer.lock());
this._cpuUpdater.update(data, this.vbToSort, this.particleTex, spawnMatrix, extentsInnerRatioUniform, emitterPos, delta, isOnStop);
// this.vertexBuffer.unlock();
}
if (!this.loop) {
if (Date.now() > this.endTime) {
if (this.onFinished) this.onFinished();
this.meshInstance.visible = false;
}
}
if (this.meshInstance) {
this.meshInstance.drawOrder = this.drawOrder;
}
this._addTimeTime += now() - startTime;
}
_destroyResources() {
var _this_particleTexIN, _this_particleTexOUT, _this_rtParticleTexIN, _this_rtParticleTexOUT, _this_internalTex0, _this_internalTex1, _this_internalTex2, _this_internalTex3, _this_colorParam, _this_vertexBuffer, _this_indexBuffer, _this_material;
(_this_particleTexIN = this.particleTexIN) == null ? undefined : _this_particleTexIN.destroy();
this.particleTexIN = null;
(_this_particleTexOUT = this.particleTexOUT) == null ? undefined : _this_particleTexOUT.destroy();
this.particleTexOUT = null;
if (this.particleTexStart && this.particleTexStart.destroy) {
this.particleTexStart.destroy();
this.particleTexStart = null;
}
(_this_rtParticleTexIN = this.rtParticleTexIN) == null ? undefined : _this_rtParticleTexIN.destroy();
this.rtParticleTexIN = null;
(_this_rtParticleTexOUT = this.rtParticleTexOUT) == null ? undefined : _this_rtParticleTexOUT.destroy();
this.rtParticleTexOUT = null;
(_this_internalTex0 = this.internalTex0) == null ? undefined : _this_internalTex0.destroy();
this.internalTex0 = null;
(_this_internalTex1 = this.internalTex1) == null ? undefined : _this_internalTex1.destroy();
this.internalTex1 = null;
(_this_internalTex2 = this.internalTex2) == null ? undefined : _this_internalTex2.destroy();
this.internalTex2 = null;
(_this_internalTex3 = this.internalTex3) == null ? undefined : _this_internalTex3.destroy();
this.internalTex3 = null;
(_this_colorParam = this.colorParam) == null ? undefined : _this_colorParam.destroy();
this.colorParam = null;
(_this_vertexBuffer = this.vertexBuffer) == null ? undefined : _this_vertexBuffer.destroy();
this.vertexBuffer = undefined; // we are testing if vb is undefined in some code, no idea why
(_this_indexBuffer = this.indexBuffer) == null ? undefined : _this_indexBuffer.destroy();
this.indexBuffer = undefined;
(_this_material = this.material) == null ? undefined : _this_material.destroy();
this.material = null;
// note: shaders should not be destroyed as they could be shared between emitters
}
destroy() {
this.camera = null;
this._destroyResources();
}
constructor(graphicsDevice, options){
/** @type {ParticleMaterial|null} */ this.material = null;
/** @type {Texture|null} */ this.internalTex0 = null;
/** @type {Texture|null} */ this.internalTex1 = null;
/** @type {Texture|null} */ this.internalTex2 = null;
/** @type {Texture|null} */ this.colorParam = null;
this.graphicsDevice = graphicsDevice;
var gd = graphicsDevice;
var precision = 32;
this.precision = precision;
this._addTimeTime = 0;
// Global system parameters
setPropertyTarget = this;
setPropertyOptions = options;
setProperty('numParticles', 1); // Amount of particles allocated (max particles = max GL texture width at this moment)
if (this.numParticles > graphicsDevice.maxTextureSize) {
Debug.warn("WARNING: can't create more than " + graphicsDevice.maxTextureSize + " particles on this device.");
this.numParticles = graphicsDevice.maxTextureSize;
}
setProperty('rate', 1); // Emission rate
setProperty('rate2', this.rate);
setProperty('lifetime', 50); // Particle lifetime
setProperty('emitterExtents', new Vec3(0, 0, 0)); // Spawn point divergence
setProperty('emitterExtentsInner', new Vec3(0, 0, 0)); // Volume inside emitterExtents to exclude from regeneration
setProperty('emitterRadius', 0);
setProperty('emitterRadiusInner', 0); // Same as ExtentsInner but for spherical volume
setProperty('emitterShape', EMITTERSHAPE_BOX);
setProperty('initialVelocity', 1);
setProperty('wrap', false);
setProperty('localSpace', false);
setProperty('screenSpace', false);
setProperty('wrapBounds', null);
setProperty('colorMap', this.defaultParamTexture);
setProperty('normalMap', null);
setProperty('loop', true);
setProperty('preWarm', false);
setProperty('sort', PARTICLESORT_NONE); // Sorting mode: 0 = none, 1 = by distance, 2 = by life, 3 = by -life; Forces CPU mode if not 0
setProperty('mode', PARTICLEMODE_GPU);
setProperty('scene', null);
setProperty('lighting', false);
setProperty('halfLambert', false);
setProperty('intensity', 1.0);
setProperty('stretch', 0.0);
setProperty('alignToMotion', false);
setProperty('depthSoftening', 0);
setProperty('mesh', null); // Mesh to be used as particle. Vertex buffer is supposed to hold vertex position in first 3 floats of each vertex
// Leave undefined to use simple quads
setProperty('particleNormal', new Vec3(0, 1, 0));
setProperty('orientation', PARTICLEORIENTATION_SCREEN);
setProperty('depthWrite', false);
setProperty('noFog', false);
setProperty('blendType', BLEND_NORMAL);
setProperty('node', null);
setProperty('startAngle', 0);
setProperty('startAngle2', this.startAngle);
setProperty('animTilesX', 1);
setProperty('animTilesY', 1);
setProperty('animStartFrame', 0);
setProperty('animNumFrames', 1);
setProperty('animNumAnimations', 1);
setProperty('animIndex', 0);
setProperty('randomizeAnimIndex', false);
setProperty('animSpeed', 1);
setProperty('animLoop', true);
this._gpuUpdater = new ParticleGPUUpdater(this, gd);
this._cpuUpdater = new ParticleCPUUpdater(this);
this.emitterPosUniform = new Float32Array(3);
this.wrapBoundsUniform = new Float32Array(3);
this.emitterScaleUniform = new Float32Array([
1,
1,
1
]);
// Time-dependent parameters
setProperty('colorGraph', default1Curve3);
setProperty('colorGraph2', this.colorGraph);
setProperty('scaleGraph', default1Curve);
setProperty('scaleGraph2', this.scaleGraph);
setProperty('alphaGraph', default1Curve);
setProperty('alphaGraph2', this.alphaGraph);
setProperty('localVelocityGraph', default0Curve3);
setProperty('localVelocityGraph2', this.localVelocityGraph);
setProperty('velocityGraph', default0Curve3);
setProperty('velocityGraph2', this.velocityGraph);
setProperty('rotationSpeedGraph', default0Curve);
setProperty('rotationSpeedGraph2', this.rotationSpeedGraph);
setProperty('radialSpeedGraph', default0Curve);
setProperty('radialSpeedGraph2', this.radialSpeedGraph);
this.animTilesParams = new Float32Array(2);
this.animParams = new Float32Array(4);
this.animIndexParams = new Float32Array(2);
this.vbToSort = null;
this.vbOld = null;
this.particleDistance = null;
this.camera = null;
this.swapTex = false;
this.useMesh = true;
this.useCpu = !graphicsDevice.supportsGpuParticles;
this.pack8 = true;
this.localBounds = new BoundingBox();
this.worldBoundsNoTrail = new BoundingBox();
this.worldBoundsTrail = [
new BoundingBox(),
new BoundingBox()
];
this.worldBounds = new BoundingBox();
this.worldBoundsSize = new Vec3();
this.prevWorldBoundsSize = new Vec3();
this.prevWorldBoundsCenter = new Vec3();
this.prevEmitterExtents = this.emitterExtents;
this.prevEmitterRadius = this.emitterRadius;
this.worldBoundsMul = new Vec3();
this.worldBoundsAdd = new Vec3();
this.timeToSwitchBounds = 0;
// this.prevPos = new Vec3();
this.shaderParticleUpdateRespawn = null;
this.shaderParticleUpdateNoRespawn = null;
this.shaderParticleUpdateOnStop = null;
this.numParticleVerts = 0;
this.numParticleIndices = 0;
this.material = null;
this.meshInstance = null;
this.drawOrder = 0;
this.seed = Math.random();
this.fixedTimeStep = 1.0 / 60;
this.maxSubSteps = 10;
this.simTime = 0;
this.simTimeTotal = 0;
this.beenReset = false;
this._layer = null;
this.rebuild();
}
}
export { ParticleEmitter };