diff --git a/leenkx/Sources/iron/object/ParticleSystem.hx b/leenkx/Sources/iron/object/ParticleSystem.hx index a6b03da..5fab3a7 100644 --- a/leenkx/Sources/iron/object/ParticleSystem.hx +++ b/leenkx/Sources/iron/object/ParticleSystem.hx @@ -8,6 +8,8 @@ import kha.arrays.Float32Array; import iron.data.Data; import iron.data.ParticleData; import iron.data.SceneFormat; +import iron.data.Geometry; +import iron.data.MeshData; import iron.system.Time; import iron.math.Mat4; import iron.math.Quat; @@ -17,6 +19,7 @@ import iron.math.Vec4; class ParticleSystem { public var data: ParticleData; public var speed = 1.0; + public var dynamicEmitter: Bool = true; var currentSpeed = 0.0; var particles: Array; var ready: Bool; @@ -52,6 +55,12 @@ class ParticleSystem { var random = 0.0; + var tmpV4 = new Vec4(); + + var instancedData: Float32Array = null; + var lastSpawnedCount: Int = 0; + var hasUniqueGeom: Bool = false; + public function new(sceneName: String, pref: TParticleReference) { seed = pref.seed; currentSpeed = speed; @@ -62,6 +71,11 @@ class ParticleSystem { Data.getParticle(sceneName, pref.particle, function(b: ParticleData) { data = b; r = data.raw; + if (r.dynamic_emitter != null){ + dynamicEmitter = r.dynamic_emitter; + } else { + dynamicEmitter = true; + } if (Scene.active.raw.gravity != null) { gx = Scene.active.raw.gravity[0] * r.weight_gravity; gy = Scene.active.raw.gravity[1] * r.weight_gravity; @@ -98,6 +112,8 @@ class ParticleSystem { lap = 0; lapTime = 0; speed = currentSpeed; + lastSpawnedCount = 0; + instancedData = null; } public function pause() { @@ -130,8 +146,13 @@ class ParticleSystem { // Copy owner world transform but discard scale owner.transform.world.decompose(ownerLoc, ownerRot, ownerScl); - object.transform.loc = ownerLoc; - object.transform.rot = ownerRot; + if (dynamicEmitter) { + object.transform.loc.x = 0; object.transform.loc.y = 0; object.transform.loc.z = 0; + object.transform.rot = new Quat(); + } else { + object.transform.loc = ownerLoc; + object.transform.rot = ownerRot; + } // Set particle size per particle system object.transform.scale = new Vec4(r.particle_size, r.particle_size, r.particle_size, 1); @@ -158,13 +179,18 @@ class ParticleSystem { if (lap > prevLap && !r.loop) { end(); } + + if (lap > prevLap && r.loop) { + lastSpawnedCount = 0; + } updateGpu(object, owner); } public function getData(): Mat4 { var hair = r.type == 1; - m._00 = animtime; + // Store loop flag in the sign: positive -> loop, negative -> no loop + m._00 = r.loop ? animtime : -animtime; m._01 = hair ? 1 / particles.length : spawnRate; m._02 = hair ? 1 : lifetime; m._03 = particles.length; @@ -187,17 +213,26 @@ class ParticleSystem { return r.size_random; } - public function getRandom(): FastFloat { + public inline function getRandom(): FastFloat { return random; } - public function getSize(): FastFloat { + public inline function getSize(): FastFloat { return r.particle_size; } function updateGpu(object: MeshObject, owner: MeshObject) { - if (!object.data.geom.instanced) setupGeomGpu(object, owner); - // GPU particles transform is attached to owner object + if (dynamicEmitter) { + if (!hasUniqueGeom) ensureUniqueGeom(object); + var needSetup = instancedData == null || object.data.geom.instancedVB == null; + if (needSetup) setupGeomGpuDynamic(object, owner); + updateSpawnedInstances(object, owner); + } + else { + if (!hasUniqueGeom) ensureUniqueGeom(object); + if (!object.data.geom.instanced) setupGeomGpu(object, owner); + } + // GPU particles transform is attached to owner object in static mode } function setupGeomGpu(object: MeshObject, owner: MeshObject) { @@ -258,18 +293,134 @@ class ParticleSystem { object.data.geom.setupInstanced(instancedData, 1, Usage.StaticUsage); } - function fhash(n: Int): Float { - var s = n + 1.0; - s *= 9301.0 % s; - s = (s * 9301.0 + 49297.0) % 233280.0; - return s / 233280.0; + // allocate instanced VB once for this object + function setupGeomGpuDynamic(object: MeshObject, owner: MeshObject) { + if (instancedData == null) instancedData = new Float32Array(particles.length * 3); + lastSpawnedCount = 0; + // Create instanced VB once if missing (seed with our instancedData) + if (object.data.geom.instancedVB == null) { + object.data.geom.setupInstanced(instancedData, 1, Usage.DynamicUsage); + } } + function ensureUniqueGeom(object: MeshObject) { + if (hasUniqueGeom) return; + var newData: MeshData = null; + new MeshData(object.data.raw, function(dat: MeshData) { + dat.scalePos = object.data.scalePos; + dat.scaleTex = object.data.scaleTex; + dat.format = object.data.format; + newData = dat; + }); + if (newData != null) object.setData(newData); + hasUniqueGeom = true; + } + + function updateSpawnedInstances(object: MeshObject, owner: MeshObject) { + if (instancedData == null) return; + var targetCount = count; + if (targetCount > particles.length) targetCount = particles.length; + if (targetCount <= lastSpawnedCount) return; + + var normFactor = 1 / 32767; + var scalePosOwner = owner.data.scalePos; + var scalePosParticle = object.data.scalePos; + var particleSize = r.particle_size; + var base = 1.0 / (particleSize * scalePosParticle); + + switch (r.emit_from) { + case 0: // Vert + var pa = owner.data.geom.positions; + var osx = owner.transform.scale.x; + var osy = owner.transform.scale.y; + var osz = owner.transform.scale.z; + var pCount = Std.int(pa.values.length / pa.size); + for (idx in lastSpawnedCount...targetCount) { + var j = Std.int(fhash(idx) * pCount); + var lx = pa.values[j * pa.size ] * normFactor * scalePosOwner * osx; + var ly = pa.values[j * pa.size + 1] * normFactor * scalePosOwner * osy; + var lz = pa.values[j * pa.size + 2] * normFactor * scalePosOwner * osz; + tmpV4.x = lx; tmpV4.y = ly; tmpV4.z = lz; tmpV4.w = 1; + tmpV4.applyQuat(ownerRot); + var o = idx * 3; + instancedData.set(o , (tmpV4.x + ownerLoc.x) * base); + instancedData.set(o + 1, (tmpV4.y + ownerLoc.y) * base); + instancedData.set(o + 2, (tmpV4.z + ownerLoc.z) * base); + } + + case 1: // Face + var positions = owner.data.geom.positions.values; + var osx1 = owner.transform.scale.x; + var osy1 = owner.transform.scale.y; + var osz1 = owner.transform.scale.z; + for (idx in lastSpawnedCount...targetCount) { + var ia = owner.data.geom.indices[Std.random(owner.data.geom.indices.length)]; + var faceIndex = Std.random(Std.int(ia.length / 3)); + var i0 = ia[faceIndex * 3 + 0]; + var i1 = ia[faceIndex * 3 + 1]; + var i2 = ia[faceIndex * 3 + 2]; + var v0x = positions[i0 * 4 ], v0y = positions[i0 * 4 + 1], v0z = positions[i0 * 4 + 2]; + var v1x = positions[i1 * 4 ], v1y = positions[i1 * 4 + 1], v1z = positions[i1 * 4 + 2]; + var v2x = positions[i2 * 4 ], v2y = positions[i2 * 4 + 1], v2z = positions[i2 * 4 + 2]; + var rx = Math.random(); var ry = Math.random(); if (rx + ry > 1) { rx = 1 - rx; ry = 1 - ry; } + var pxs = v0x + rx * (v1x - v0x) + ry * (v2x - v0x); + var pys = v0y + rx * (v1y - v0y) + ry * (v2y - v0y); + var pzs = v0z + rx * (v1z - v0z) + ry * (v2z - v0z); + var px = pxs * normFactor * scalePosOwner * osx1; + var py = pys * normFactor * scalePosOwner * osy1; + var pz = pzs * normFactor * scalePosOwner * osz1; + tmpV4.x = px; tmpV4.y = py; tmpV4.z = pz; tmpV4.w = 1; + tmpV4.applyQuat(ownerRot); + var o1 = idx * 3; + instancedData.set(o1 , (tmpV4.x + ownerLoc.x) * base); + instancedData.set(o1 + 1, (tmpV4.y + ownerLoc.y) * base); + instancedData.set(o1 + 2, (tmpV4.z + ownerLoc.z) * base); + } + + case 2: // Volume + var dim = object.transform.dim; + for (idx in lastSpawnedCount...targetCount) { + tmpV4.x = (Math.random() * 2.0 - 1.0) * (dim.x * 0.5); + tmpV4.y = (Math.random() * 2.0 - 1.0) * (dim.y * 0.5); + tmpV4.z = (Math.random() * 2.0 - 1.0) * (dim.z * 0.5); + tmpV4.w = 1; + tmpV4.applyQuat(ownerRot); + var o2 = idx * 3; + instancedData.set(o2 , (tmpV4.x + ownerLoc.x) * base); + instancedData.set(o2 + 1, (tmpV4.y + ownerLoc.y) * base); + instancedData.set(o2 + 2, (tmpV4.z + ownerLoc.z) * base); + } + } + + // Upload full active range [0..targetCount) to this object's instanced VB + var geom = object.data.geom; + if (geom.instancedVB == null) { + geom.setupInstanced(instancedData, 1, Usage.DynamicUsage); + } + var vb = geom.instancedVB.lock(); + var totalFloats = targetCount * 3; // xyz per instance + var i = 0; + while (i < totalFloats) { + vb.setFloat32(i * 4, instancedData[i]); + i++; + } + geom.instancedVB.unlock(); + geom.instanceCount = targetCount; + lastSpawnedCount = targetCount; + } + + inline function fhash(n: Int): Float { + var s = n + 1.0; + s *= 9301.0 % s; + s = (s * 9301.0 + 49297.0) % 233280.0; + return s / 233280.0; +} + public function remove() {} /** Generates a random point in the triangle with vertex positions abc. - + Please note that the given position vectors are changed in-place by this function and can be considered garbage afterwards, so make sure to clone them first if needed.