package iron.object; #if lnx_cpu_particles import iron.Scene; import iron.data.Data; import iron.data.ParticleData; import iron.data.SceneFormat; import iron.math.Quat; import iron.math.Vec3; import iron.math.Vec4; import iron.object.MeshObject; import iron.object.Object; import iron.system.Time; import iron.system.Tween; import kha.FastFloat; import kha.arrays.Int16Array; import kha.arrays.Uint32Array; class ParticleSystemCPU { public var data: ParticleData; public var speed: FastFloat = 1.0; // Not used yet. Added to go in hand with `ParticleSystemGPU` var r: TParticleData; // Format final baseFrameRate: FastFloat = 24.0; var frameRate: FastFloat = 24.0; var type: Int = 0; // type: 0 - Emission, 1 - Hair // Emission var count: Int = 10; // count var frameStart: FastFloat = 1; // frame_start var frameEnd: FastFloat = 10.0; // frame_end var lifetime: FastFloat = 24.0; // lifetime var lifetimeRandom: FastFloat = 0.0; // lifetime_random var emitFrom: Int = 1; // emit_from: 0 - Vert, 1 - Face, 2 - Volume // TODO: fully integrate Blender's properties // Velocity var velocity: Vec3 = new Vec3(0.0, 0.0, 1.0); // object_align_factor: Float32Array var velocityRandom: FastFloat = 0.0; // factor_random // Rotation var rotation: Bool = false; // use_rotations var orientationAxis: Int = 0; // rotation_mode: 0 - None, 1 - Normal, 2 - Normal-Tangent, 3 - Velocity/Hair, 4 - Global X, 5 - Global Y, 6 - Global Z, 7 - Object X, 8 - Object Y, 9 - Object Z var rotationRandom: FastFloat = 0.0; // rotation_factor_random var phase: FastFloat = 0.0; // phase_factor var phaseRandom: FastFloat = 0.0; // phase_factor_random var dynamicRotation: Bool = false; // use_dynamic_rotation // Render var instanceObject: String; // instance_object var scale: FastFloat = 1.0; // particle_size var scaleRandom: FastFloat = 0.0; // size_random // Field weights var gravity: Vec3 = new Vec3(0, 0, -9.8); var gravityFactor: FastFloat = 1.0; // weight_gravity var textureFactor: FastFloat = 1.0; // weight_texture // Textures var textureSlots: Map = []; // Lnx props var autoStart: Bool = true; // auto_start var localCoords: Bool = false; // local_coords var loop: Bool = false; // loop // Internal logic var owner: MeshObject; var lifetimeSeconds: FastFloat = 0.0; var spawnRate: FastFloat = 0.0; var spawnFactor: Int = 1; var spawnedParticles: Int = 0; var particleScale: FastFloat = 1.0; var loopAnim: TAnim; var spawnTime: FastFloat = 0; var randQuat: Quat; var phaseQuat: Quat; // Tween scaling var scaleElementsCount: Int = 0; var scaleRampSizeFactor: FastFloat = 0; var rampPositions: Array = []; var rampColors: Array = []; // Optimization var particlePool: Array = []; var particlePhysics: Map = []; public function new(sceneName: String, pref: TParticleReference, mo: MeshObject) { Data.getParticle(sceneName, pref.particle, function (b: ParticleData) { data = b; r = data.raw; owner = mo; frameRate = r.fps; type = r.type; count = r.count; frameStart = r.frame_start; frameEnd = r.frame_end; lifetime = r.lifetime; lifetimeRandom = r.lifetime_random; emitFrom = r.emit_from; rotation = r.use_rotations; orientationAxis = r.rotation_mode; rotationRandom = r.rotation_factor_random; phase = r.phase_factor; phaseRandom = r.phase_factor_random; dynamicRotation = r.use_dynamic_rotation; instanceObject = r.instance_object; scale = r.particle_size; scaleRandom = r.size_random; velocity = new Vec3(r.object_align_factor[0], r.object_align_factor[1], r.object_align_factor[2]).mult(frameRate / baseFrameRate).mult(1 / scale); velocityRandom = r.factor_random * (frameRate / baseFrameRate); if (Scene.active.raw.gravity != null) { gravity = new Vec3(Scene.active.raw.gravity[0], Scene.active.raw.gravity[1], Scene.active.raw.gravity[2]).mult(frameRate / baseFrameRate).mult(1 / scale); } gravityFactor = r.weight_gravity * (frameRate / baseFrameRate); textureFactor = r.weight_texture; if (r.texture_slots != null) { for (slot in Reflect.fields(r.texture_slots)) { textureSlots[slot] = Reflect.field(r.texture_slots, slot); } } autoStart = r.auto_start; localCoords = r.local_coords; loop = r.loop; spawnRate = ((frameEnd - frameStart) / count) / frameRate; lifetimeSeconds = lifetime / frameRate; scaleElementsCount = getRampElementsLength(); scaleRampSizeFactor = getRampSizeFactor(); Scene.active.notifyOnInit(function () { var i: Int; for (i in 0...count) addToPool(); }); switch (type) { case 0: // Emission loopAnim = { tick: function () { spawnTime += Time.delta * Time.scale; var expected: Int = Math.floor(spawnTime / spawnRate); while (spawnedParticles < expected && spawnedParticles < count) { spawnParticle(); spawnedParticles++; } updateParticles(); }, target: null, props: null, duration: loop ? lifetimeSeconds : lifetimeSeconds * 2, done: function () { if (loop) start(); } } case 1: // Hair Scene.active.notifyOnInit(function () { var i: Int; for (i in 0...count) spawnParticle(); }); default: } Scene.active.notifyOnInit(function () { if (autoStart) start(); }); }); } public function start() { if (type != 0) return; spawnTime = 0; spawnedParticles = 0; Tween.to(loopAnim); } // TODO public function pause() { } // TODO public function resume() { } public function stop() { if (type != 0) return; spawnTime = 0; spawnedParticles = 0; Tween.stop(loopAnim); for (particle => physics in particlePhysics) releaseParticle(particle); particlePhysics.clear(); } function addToPool() { Scene.active.spawnObject(instanceObject, localCoords ? owner : null, function (o: Object) { o.visible = false; particlePool.push(o); }); } function getFreeParticle(): Object { for (particle in particlePool) { if (!particle.visible) { particle.visible = true; return particle; } } return null; } function releaseParticle(o: Object) { o.visible = false; o.transform.loc = new Vec4(); o.transform.rot = new Quat(); o.transform.scale = new Vec4(1, 1, 1, 1); } function spawnParticle() { var o: Object = getFreeParticle(); if (o == null) { addToPool(); o = getFreeParticle(); } owner.transform.buildMatrix(); var objectPos: Vec4 = new Vec4(); var objectRot: Quat = new Quat(); var objectScale: Vec4 = new Vec4(); owner.transform.world.decompose(objectPos, objectRot, objectScale); o.visible = true; var normFactor: FastFloat = 1 / 32767; var scalePos: FastFloat = owner.data.scalePos; var scalePosParticle: FastFloat = cast(o, MeshObject).data.scalePos; // TODO: add all properties from Blender's UI switch (emitFrom) { case 0: // Vertices var pa: TVertexArray = owner.data.geom.positions; var i: Int = Std.int(Math.random() * (pa.values.length / pa.size)); var loc: Vec4 = new Vec4(pa.values[i * pa.size] * normFactor, pa.values[i * pa.size + 1] * normFactor, pa.values[i * pa.size + 2] * normFactor, 1); if (!localCoords) { loc.applyQuat(objectRot); loc.add(objectPos); } o.transform.loc.setFrom(loc); case 1: // Faces var positions: Int16Array = owner.data.geom.positions.values; var ia: Uint32Array = owner.data.geom.indices[Std.random(owner.data.geom.indices.length)]; var faceIndex: Int = 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 v0: Vec3 = new Vec3(positions[i0 * 4], positions[i0 * 4 + 1], positions[i0 * 4 + 2]); var v1: Vec3 = new Vec3(positions[i1 * 4], positions[i1 * 4 + 1], positions[i1 * 4 + 2]); var v2: Vec3 = new Vec3(positions[i2 * 4], positions[i2 * 4 + 1], positions[i2 * 4 + 2]); var pos: Vec3 = randomPointInTriangle(v0, v1, v2); var loc: Vec4 = new Vec4(pos.x, pos.y, pos.z, 1).mult(normFactor); if (!localCoords) { loc.applyQuat(objectRot); loc.add(objectPos); } o.transform.loc.setFrom(loc); case 2: // Volume var scaleFactorVolume: Vec4 = new Vec4().setFrom(owner.transform.dim); scaleFactorVolume.mult(0.5); var loc: Vec4 = new Vec4((Math.random() * 2.0 - 1.0) * scaleFactorVolume.x, (Math.random() * 2.0 - 1.0) * scaleFactorVolume.y, (Math.random() * 2.0 - 1.0) * scaleFactorVolume.z, 1); if (!localCoords) { loc.applyQuat(objectRot); loc.add(objectPos); } o.transform.loc.setFrom(loc); } particleScale = 1 - scaleRandom * Math.random(); var localFactor: Vec3 = localCoords ? new Vec3(objectScale.x, objectScale.y, objectScale.z) : new Vec3(1, 1, 1); var sc: Vec4 = new Vec4(o.transform.scale.x / localFactor.x, o.transform.scale.y / localFactor.y, o.transform.scale.z / localFactor.z, 1.0).mult(scale).mult(particleScale); var randomLifetime: FastFloat = lifetimeSeconds * (1 - Math.random() * lifetimeRandom); if (scaleElementsCount != 0) { rampPositions = getRampPositions(); rampColors = getRampColors(); } else { o.transform.scale.setFrom(sc); } o.transform.buildMatrix(); var randomX: FastFloat = (Math.random() * 2 / (scale * particleScale) - 1 / (scale * particleScale)) * velocityRandom; var randomY: FastFloat = (Math.random() * 2 / (scale * particleScale) - 1 / (scale * particleScale)) * velocityRandom; var randomZ: FastFloat = (Math.random() * 2 / (scale * particleScale) - 1 / (scale * particleScale)) * velocityRandom; var g: Vec3 = new Vec3(); var rotatedVelocity: Vec4 = new Vec4(velocity.x + randomX, velocity.y + randomY, velocity.z + randomZ, 1); if (!localCoords) rotatedVelocity.applyQuat(objectRot); if (rotation) { // Rotation phase and randomness. Wrap values between -1 and 1. randQuat = new Quat().fromEuler((Math.random() * 2 - 1) * Math.PI * rotationRandom, (Math.random() * 2 - 1) * Math.PI * rotationRandom, (Math.random() * 2 - 1) * Math.PI * rotationRandom); var phaseRand: FastFloat = (Math.random() * 2 - 1) * phaseRandom; var phaseValue: FastFloat = phase + phaseRand; while (phaseValue > 1) phaseValue -= 2; while (phaseValue < -1) phaseValue += 2; var dirQuat: Quat = new Quat(); phaseQuat = new Quat().fromEuler(0, phaseValue * Math.PI, 0); switch (orientationAxis) { case 0: // None o.transform.rotate(new Vec4(0, 0, 1, 1), -Math.PI * 0.5); case 1: // Normal case 2: // Normal-Tangent case 3: // Velocity/Hair setVelocityHair(o, rotatedVelocity, randQuat, phaseQuat); case 4: // Global X o.transform.rot.fromEuler(0, 0, -Math.PI * 0.5).mult(phaseQuat).mult(randQuat); case 5: // Global Y o.transform.rot.fromEuler(0, 0, 0).mult(phaseQuat).mult(randQuat); case 6: // Global Z o.transform.rot.fromEuler(0, -Math.PI * 0.5, -Math.PI * 0.5).mult(phaseQuat).mult(randQuat); case 7: // Object X o.transform.rot.setFrom(objectRot); dirQuat.fromEuler(0, 0, -Math.PI * 0.5); o.transform.rot.mult(dirQuat).mult(phaseQuat).mult(randQuat); case 8: // Object Y o.transform.rot.setFrom(objectRot); o.transform.rot.mult(phaseQuat).mult(randQuat); case 9: // Object Z o.transform.rot.setFrom(objectRot); dirQuat.fromEuler(0, -Math.PI * 0.5, 0).mult(new Quat().fromEuler(0, 0, -Math.PI * 0.5)); o.transform.rot.mult(dirQuat).mult(phaseQuat).mult(randQuat); default: } } else { o.transform.rotate(new Vec4(0, 0, 1, 1), -Math.PI * 0.5); } var physics: TParticlePhysics = { velocity: rotatedVelocity.clone(), gravity: gravity.clone().mult(gravityFactor), lifetime: randomLifetime, age: 0.0, hasScaleRamp: scaleElementsCount != 0, baseScale: sc.clone(), rampPositions: rampPositions.copy(), rampColors: rampColors.copy(), scaleRampSizeFactor: scaleRampSizeFactor }; particlePhysics.set(o, physics); o.transform.buildMatrix(); } } function setVelocityHair(object: Object, velocity: Vec4, randQuat: Quat, phaseQuat: Quat) { var dir: Vec4 = velocity.clone().normalize(); var yaw: FastFloat = Math.atan2(-dir.x, dir.y); var pitch: FastFloat = Math.asin(dir.z); var targetRot: Quat = new Quat().fromEuler(pitch, 0, yaw); targetRot.mult(randQuat); object.transform.rot.setFrom(targetRot.mult(phaseQuat)); } function updateParticles() { for (particle => physics in particlePhysics) { physics.age += Time.delta * Time.scale; if (physics.age >= physics.lifetime) { particlePhysics.remove(particle); releaseParticle(particle); continue; } physics.velocity.x += physics.gravity.x * Time.delta * Time.scale; physics.velocity.y += physics.gravity.y * Time.delta * Time.scale; physics.velocity.z += physics.gravity.z * Time.delta * Time.scale; particle.transform.translate( physics.velocity.x * Time.delta * Time.scale, physics.velocity.y * Time.delta * Time.scale, physics.velocity.z * Time.delta * Time.scale ); if (rotation && dynamicRotation && orientationAxis == 3) setVelocityHair(particle, physics.velocity, randQuat, phaseQuat); if (physics.hasScaleRamp && physics.rampPositions.length > 1) { var normalizedAge: FastFloat = physics.age / physics.lifetime; var scaleMultiplier: FastFloat = interpolateRampValue(normalizedAge, physics.rampPositions, physics.rampColors); var finalScale: FastFloat = scale * (particleScale * (1 - physics.scaleRampSizeFactor) + scaleMultiplier * physics.scaleRampSizeFactor); particle.transform.scale.setFrom(physics.baseScale.clone().mult(finalScale)); } particle.transform.buildMatrix(); } } // Linear interpolation function interpolateRampValue(normalizedAge: FastFloat, positions: Array, colors: Array): FastFloat { if (positions.length == 0) return 1.0; if (normalizedAge <= positions[0]) return colors[0]; if (normalizedAge >= positions[positions.length - 1]) return colors[colors.length - 1]; var i: Int; for (i in 0...(positions.length - 1)) { if (normalizedAge >= positions[i] && normalizedAge <= positions[i + 1]) { var t: FastFloat = (normalizedAge - positions[i]) / (positions[i + 1] - positions[i]); return colors[i] + t * (colors[i + 1] - colors[i]); } } return colors[colors.length - 1]; } function getRampSizeFactor(): FastFloat { // Just using the first slot for now: 1 texture slot // TODO: use all available slots ? for (slot in textureSlots.keys()) { var s: Dynamic = textureSlots[slot]; if (s != null && s.use_map_size) { var sizeFactor: FastFloat = s.size_factor; return sizeFactor * textureFactor; } } return 0.0; } function getRampElementsLength(): Int { for (slot in textureSlots.keys()) { var s: Dynamic = textureSlots[slot]; if (s == null) continue; var tex: Dynamic = s.texture; if (tex == null) continue; if (tex.use_color_ramp) { var ramp: Dynamic = tex.color_ramp; if (ramp == null) continue; var elems: Dynamic = ramp.elements; if (elems == null) continue; return elems.length; } } return 0; } function getRampPositions(): Array { // Just using the first slot for now: 1 texture slot // TODO: use all available slots ? for (slot in textureSlots.keys()) { var s: Dynamic = textureSlots[slot]; if (s == null) continue; var tex: Dynamic = s.texture; if (tex == null) continue; if (tex.use_color_ramp) { var ramp: Dynamic = tex.color_ramp; if (ramp == null) continue; var elems: Dynamic = ramp.elements; if (elems == null) continue; var positions: Array = []; for (i in 0...elems.length) { positions.push(elems[i].position); } return positions; } } return []; } function getRampColors(): Array { // Just using the first slot for now: 1 texture slot // TODO: use all available slots ? for (slot in textureSlots.keys()) { var s: Dynamic = textureSlots[slot]; if (s == null) continue; var tex: Dynamic = s.texture; if (tex == null) continue; if (tex.use_color_ramp) { var ramp: Dynamic = tex.color_ramp; if (ramp == null) continue; var elems: Dynamic = ramp.elements; if (elems == null) continue; var colors: Array = []; for (i in 0...elems.length) { colors.push(elems[i].color.b); // Just need R, G or B for black and white images. Using B as it can be interpreted as V with HSV } return colors; } } return []; } public function remove() { for (particle in particlePool) particle.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. **/ public static inline function randomPointInTriangle(a: Vec3, b: Vec3, c: Vec3): Vec3 { // Generate a random point in a square where (0, 0) <= (x, y) < (1, 1) var x = Math.random(); var y = Math.random(); if (x + y > 1) { // We're in the upper right triangle in the square, mirror to lower left x = 1 - x; y = 1 - y; } // Transform the point to the triangle abc var u = b.sub(a); var v = c.sub(a); return a.add(u.mult(x).add(v.mult(y))); } typedef TParticlePhysics = { var velocity: Vec4; var gravity: Vec3; var lifetime: Float; var age: Float; var hasScaleRamp: Bool; var baseScale: Vec4; var rampPositions: Array; var rampColors: Array; var scaleRampSizeFactor: FastFloat; } } #end