Files
LNXSDK/leenkx/Sources/iron/object/ParticleSystemCPU.hx

552 lines
18 KiB
Haxe

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<String, Dynamic> = [];
// 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<FastFloat> = [];
var rampColors: Array<FastFloat> = [];
// Optimization
var particlePool: Array<Object> = [];
var particlePhysics: Map<Object, TParticlePhysics> = [];
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<FastFloat>, colors: Array<FastFloat>): 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<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) 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<FastFloat> = [];
for (i in 0...elems.length) {
positions.push(elems[i].position);
}
return positions;
}
}
return [];
}
function getRampColors(): Array<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) 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<FastFloat> = [];
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<FastFloat>;
var rampColors: Array<FastFloat>;
var scaleRampSizeFactor: FastFloat;
}
}
#end