forked from LeenkxTeam/LNXSDK
Repe [T3DU] and Moises Jpelaez updates
This commit is contained in:
@ -14,7 +14,7 @@ class Animation {
|
||||
|
||||
public var isSkinned: Bool;
|
||||
public var isSampled: Bool;
|
||||
public var action = "";
|
||||
@:isVar public var action(get, default) = "";
|
||||
#if lnx_skin
|
||||
public var armature: iron.data.Armature; // Bone
|
||||
#end
|
||||
@ -57,6 +57,10 @@ class Animation {
|
||||
play();
|
||||
}
|
||||
|
||||
function get_action(): String {
|
||||
return action;
|
||||
}
|
||||
|
||||
public function play(action = "", onComplete: Void->Void = null, blendTime = 0.0, speed = 1.0, loop = true) {
|
||||
if (blendTime > 0) {
|
||||
this.blendTime = blendTime;
|
||||
@ -98,7 +102,7 @@ class Animation {
|
||||
else {
|
||||
sampler.timeOld = sampler.time;
|
||||
sampler.offsetOld = sampler.offset;
|
||||
sampler.setTimeOnly(sampler.time + delta * sampler.speed);
|
||||
sampler.setTimeOnly(sampler.time + delta * sampler.speed * iron.system.Time.scale);
|
||||
updateActionTrack(sampler);
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,6 +14,8 @@ import iron.data.Armature;
|
||||
import iron.data.Data;
|
||||
import iron.math.Ray;
|
||||
|
||||
import StringTools;
|
||||
|
||||
class BoneAnimation extends Animation {
|
||||
|
||||
public static var skinMaxBones = 128;
|
||||
@ -84,6 +86,15 @@ class BoneAnimation extends Animation {
|
||||
}
|
||||
}
|
||||
|
||||
override function get_action(): String {
|
||||
var an: String = action; // an -> action name
|
||||
if (an != "" && object != null && object.filename != "") {
|
||||
var sufix = "_" + object.filename;
|
||||
if (an.indexOf(sufix) != -1) an = StringTools.replace(an, sufix, "");
|
||||
}
|
||||
return an;
|
||||
}
|
||||
|
||||
public function initMatsEmpty(): Array<Mat4> {
|
||||
|
||||
var mats = [];
|
||||
@ -135,7 +146,16 @@ class BoneAnimation extends Animation {
|
||||
if (ar != null) ar.remove(o);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getName(n: String): String {
|
||||
var fn: String = n; // fn -> final name
|
||||
if (fn != "" && object != null && object.filename != "") {
|
||||
var sufix = "_" + object.filename;
|
||||
if (fn.indexOf(sufix) == -1) fn += sufix;
|
||||
}
|
||||
return fn;
|
||||
}
|
||||
|
||||
@:access(iron.object.Transform)
|
||||
function updateBoneChildren(bone: TObj, bm: Mat4) {
|
||||
var ar = boneChildren.get(bone.name);
|
||||
@ -232,10 +252,11 @@ class BoneAnimation extends Animation {
|
||||
}
|
||||
|
||||
override public function play(action = "", onComplete: Void->Void = null, blendTime = 0.2, speed = 1.0, loop = true) {
|
||||
if (action != "") {
|
||||
setAction(action);
|
||||
super.play(action, onComplete, blendTime, speed, loop);
|
||||
var tempAnimParam = new ActionSampler(action);
|
||||
var actionName: String = getName(action);
|
||||
if (actionName != "") {
|
||||
setAction(actionName);
|
||||
super.play(actionName, onComplete, blendTime, speed, loop);
|
||||
var tempAnimParam = new ActionSampler(actionName);
|
||||
registerAction("tempAction", tempAnimParam);
|
||||
updateAnimation = function(mats){
|
||||
sampleAction(tempAnimParam, mats);
|
||||
|
||||
@ -370,8 +370,6 @@ class LightObject extends Object {
|
||||
}
|
||||
#end // lnx_csm
|
||||
|
||||
#if lnx_clusters
|
||||
|
||||
// Centralize discarding conditions when iterating over lights
|
||||
// Important to avoid issues later with "misaligned" data in uniforms (lightsArray, clusterData, LWVPSpotArray)
|
||||
public inline static function discardLight(light: LightObject) {
|
||||
@ -381,6 +379,8 @@ class LightObject extends Object {
|
||||
public inline static function discardLightCulled(light: LightObject) {
|
||||
return #if lnx_shadowmap_atlas light.culledLight || #end discardLight(light);
|
||||
}
|
||||
|
||||
#if lnx_clusters
|
||||
|
||||
#if (lnx_shadowmap_atlas && lnx_shadowmap_atlas_lod)
|
||||
// Arbitrary function to map from [0-16] to [1.0-0.0]
|
||||
@ -422,7 +422,8 @@ class LightObject extends Object {
|
||||
|
||||
#if lnx_spot // Point lamps first
|
||||
lights.sort(function(a, b): Int {
|
||||
return a.data.raw.type >= b.data.raw.type ? 1 : -1;
|
||||
if (a.data.raw.type == b.data.raw.type) return 0;
|
||||
return a.data.raw.type > b.data.raw.type ? 1 : -1;
|
||||
});
|
||||
#end
|
||||
|
||||
@ -494,6 +495,13 @@ class LightObject extends Object {
|
||||
continue;
|
||||
}
|
||||
#end
|
||||
if (minX < 0) minX = 0;
|
||||
if (maxX >= slicesX) maxX = slicesX - 1;
|
||||
if (minY < 0) minY = 0;
|
||||
if (maxY >= slicesY) maxY = slicesY - 1;
|
||||
if (minZ < 0) minZ = 0;
|
||||
if (maxZ >= slicesZ) maxZ = slicesZ - 1;
|
||||
|
||||
// Mark affected clusters
|
||||
for (z in minZ...maxZ + 1) {
|
||||
for (y in minY...maxY + 1) {
|
||||
|
||||
@ -18,17 +18,18 @@ class MeshObject extends Object {
|
||||
public var depthRead(default, null) = false;
|
||||
#if lnx_particles
|
||||
public var particleSystems: Array<ParticleSystem> = null; // Particle owner
|
||||
public var render_emitter = true;
|
||||
#end
|
||||
#if lnx_gpu_particles
|
||||
public var particleChildren: Array<MeshObject> = null;
|
||||
public var particleOwner: MeshObject = null; // Particle object
|
||||
public var particleIndex = -1;
|
||||
public var render_emitter = true;
|
||||
#end
|
||||
public var cameraDistance: Float;
|
||||
public var cameraList: Array<String> = null;
|
||||
public var screenSize = 0.0;
|
||||
public var frustumCulling = true;
|
||||
public var activeTilesheet: Tilesheet = null;
|
||||
public var tilesheets: Array<Tilesheet> = null;
|
||||
public var tilesheet: Tilesheet = null;
|
||||
public var skip_context: String = null; // Do not draw this context
|
||||
public var force_context: String = null; // Draw only this context
|
||||
static var lastPipeline: PipelineState = null;
|
||||
@ -72,18 +73,22 @@ class MeshObject extends Object {
|
||||
#if lnx_batch
|
||||
Scene.active.meshBatch.removeMesh(this);
|
||||
#end
|
||||
#if lnx_particles
|
||||
#if lnx_gpu_particles
|
||||
if (particleChildren != null) {
|
||||
for (c in particleChildren) c.remove();
|
||||
particleChildren = null;
|
||||
}
|
||||
#end
|
||||
#if lnx_particles
|
||||
if (particleSystems != null) {
|
||||
for (psys in particleSystems) psys.remove();
|
||||
for (psys in particleSystems) {
|
||||
#if lnx_cpu_particles psys.stop(); #end
|
||||
psys.remove();
|
||||
}
|
||||
particleSystems = null;
|
||||
}
|
||||
#end
|
||||
if (activeTilesheet != null) activeTilesheet.remove();
|
||||
if (tilesheets != null) for (ts in tilesheets) { ts.remove(); }
|
||||
if (tilesheet != null) tilesheet.remove();
|
||||
if (Scene.active != null) Scene.active.meshes.remove(this);
|
||||
data.refcount--;
|
||||
super.remove();
|
||||
@ -113,35 +118,19 @@ class MeshObject extends Object {
|
||||
#if lnx_particles
|
||||
public function setupParticleSystem(sceneName: String, pref: TParticleReference) {
|
||||
if (particleSystems == null) particleSystems = [];
|
||||
var psys = new ParticleSystem(sceneName, pref);
|
||||
var psys = new ParticleSystem(sceneName, pref, this);
|
||||
particleSystems.push(psys);
|
||||
}
|
||||
#end
|
||||
|
||||
public function setupTilesheet(sceneName: String, tilesheet_ref: String, tilesheet_action_ref: String) {
|
||||
activeTilesheet = new Tilesheet(sceneName, tilesheet_ref, tilesheet_action_ref);
|
||||
if(tilesheets == null) tilesheets = new Array<Tilesheet>();
|
||||
tilesheets.push(activeTilesheet);
|
||||
public function setupTilesheet(tilesheetData: iron.data.SceneFormat.TTilesheetData) {
|
||||
tilesheet = new Tilesheet(tilesheetData, this);
|
||||
}
|
||||
|
||||
public function setActiveTilesheet(sceneName: String, tilesheet_ref: String, tilesheet_action_ref: String) {
|
||||
var set = false;
|
||||
// Check if tilesheet already created
|
||||
if (tilesheets != null) {
|
||||
for (ts in tilesheets) {
|
||||
if (ts.raw.name == tilesheet_ref) {
|
||||
activeTilesheet = ts;
|
||||
activeTilesheet.play(tilesheet_action_ref);
|
||||
set = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
public function setTilesheetAction(actionRef: String) {
|
||||
if (tilesheet != null) {
|
||||
tilesheet.play(actionRef);
|
||||
}
|
||||
// If not already created
|
||||
if (!set) {
|
||||
setupTilesheet(sceneName, tilesheet_ref, tilesheet_action_ref);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
inline function isLodMaterial(): Bool {
|
||||
@ -179,7 +168,7 @@ class MeshObject extends Object {
|
||||
// Scale radius for skinned mesh and particle system
|
||||
// TODO: define skin & particle bounds
|
||||
var radiusScale = data.isSkinned ? 2.0 : 1.0;
|
||||
#if lnx_particles
|
||||
#if lnx_gpu_particles
|
||||
// particleSystems for update, particleOwner for render
|
||||
if (particleSystems != null || particleOwner != null) radiusScale *= 1000;
|
||||
#end
|
||||
@ -236,9 +225,14 @@ class MeshObject extends Object {
|
||||
if (cullMesh(context, Scene.active.camera, RenderPath.active.light)) return;
|
||||
var meshContext = raw != null ? context == "mesh" : false;
|
||||
|
||||
// Update tilesheet
|
||||
if (tilesheet != null && meshContext) {
|
||||
tilesheet.update();
|
||||
}
|
||||
|
||||
if (cameraList != null && cameraList.indexOf(Scene.active.camera.name) < 0) return;
|
||||
|
||||
#if lnx_particles
|
||||
#if lnx_gpu_particles
|
||||
if (raw != null && raw.is_particle && particleOwner == null) return; // Instancing not yet set-up by particle system owner
|
||||
if (particleSystems != null && meshContext) {
|
||||
if (particleChildren == null) {
|
||||
@ -257,9 +251,11 @@ class MeshObject extends Object {
|
||||
}
|
||||
}
|
||||
for (i in 0...particleSystems.length) {
|
||||
particleSystems[i].update(particleChildren[i], this);
|
||||
particleSystems[i].update(particleChildren[i]);
|
||||
}
|
||||
}
|
||||
#end
|
||||
#if lnx_particles
|
||||
if (particleSystems != null && particleSystems.length > 0 && !render_emitter) return;
|
||||
if (particleSystems == null && cullMaterial(context)) return;
|
||||
#else
|
||||
|
||||
@ -11,6 +11,7 @@ class Object {
|
||||
public var raw: TObj = null;
|
||||
|
||||
public var name: String = "";
|
||||
public var filename: String = "";
|
||||
public var transform: Transform;
|
||||
public var constraints: Array<Constraint> = null;
|
||||
public var traits: Array<Trait> = [];
|
||||
@ -111,12 +112,15 @@ class Object {
|
||||
**/
|
||||
public function getChild(name: String): Object {
|
||||
if (this.name == name) return this;
|
||||
else {
|
||||
for (c in children) {
|
||||
var r = c.getChild(name);
|
||||
if (r != null) return r;
|
||||
}
|
||||
else if (this.filename != "") {
|
||||
if (this.name == name + "_" + this.filename) return this;
|
||||
}
|
||||
|
||||
for (c in children) {
|
||||
var r = c.getChild(name);
|
||||
if (r != null) return r;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -209,6 +213,16 @@ class Object {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getTraitFromChildren<T: Trait>(c: Class<T>): T {
|
||||
var t: T = getTrait(c);
|
||||
if (t != null) return t;
|
||||
for (child in getChildren(true)) {
|
||||
t = child.getTraitFromChildren(c);
|
||||
if (t != null) return t;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
#if lnx_skin
|
||||
public function getBoneAnimation(armatureUid: Int): BoneAnimation {
|
||||
for (a in Scene.active.animations) {
|
||||
@ -218,10 +232,21 @@ class Object {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getParentArmature(name: String): BoneAnimation {
|
||||
for (a in Scene.active.animations) {
|
||||
if (a.armature != null && a.armature.name == name) return cast a;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
#else
|
||||
public function getBoneAnimation(armatureUid): Animation {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getParentArmature(name: String): Animation {
|
||||
return null;
|
||||
}
|
||||
#end
|
||||
|
||||
public function getObjectAnimation(): ObjectAnimation {
|
||||
@ -229,6 +254,15 @@ class Object {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getAnimation(): Null<Animation> {
|
||||
if (animation != null) return animation;
|
||||
for (c in getChildren(true)) {
|
||||
var a = c.getAnimation();
|
||||
if (a != null) return a;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function setupAnimation(oactions: Array<TSceneFormat> = null) {
|
||||
// Parented to bone
|
||||
#if lnx_skin
|
||||
|
||||
@ -9,6 +9,7 @@ import iron.math.Vec4;
|
||||
import iron.math.Mat4;
|
||||
import iron.math.Quat;
|
||||
import iron.data.SceneFormat;
|
||||
import StringTools;
|
||||
|
||||
class ObjectAnimation extends Animation {
|
||||
|
||||
@ -42,13 +43,24 @@ class ObjectAnimation extends Animation {
|
||||
isSkinned = false;
|
||||
super();
|
||||
}
|
||||
|
||||
override function get_action(): String {
|
||||
var an: String = action; // an -> action name
|
||||
if (an != "" && object != null && object.filename != "") {
|
||||
var sufix = "_" + object.filename;
|
||||
if (an.indexOf(sufix) != -1) an = StringTools.replace(an, sufix, "");
|
||||
}
|
||||
return an;
|
||||
}
|
||||
|
||||
function getAction(action: String): TObj {
|
||||
for (a in oactions) if (a != null && a.objects[0].name == action) return a.objects[0];
|
||||
return null;
|
||||
}
|
||||
|
||||
override public function play(action = "", onComplete: Void->Void = null, blendTime = 0.0, speed = 1.0, loop = true) {
|
||||
super.play(action, onComplete, blendTime, speed, loop);
|
||||
var actionName: String = object != null && object.filename != "" ? action + "_" + object.filename : action;
|
||||
super.play(actionName, onComplete, blendTime, speed, loop);
|
||||
if (this.action == "" && oactions != null && oactions[0] != null){
|
||||
this.action = oactions[0].objects[0].name;
|
||||
}
|
||||
|
||||
@ -1,461 +1,9 @@
|
||||
package iron.object;
|
||||
|
||||
#if lnx_particles
|
||||
|
||||
import kha.FastFloat;
|
||||
import kha.graphics4.Usage;
|
||||
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;
|
||||
import iron.math.Vec3;
|
||||
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<Particle>;
|
||||
var ready: Bool;
|
||||
var frameRate = 24;
|
||||
var lifetime = 0.0;
|
||||
var looptime = 0.0;
|
||||
var animtime = 0.0;
|
||||
var time = 0.0;
|
||||
var spawnRate = 0.0;
|
||||
var seed = 0;
|
||||
|
||||
var r: TParticleData;
|
||||
var gx: Float;
|
||||
var gy: Float;
|
||||
var gz: Float;
|
||||
var alignx: Float;
|
||||
var aligny: Float;
|
||||
var alignz: Float;
|
||||
var dimx: Float;
|
||||
var dimy: Float;
|
||||
var tilesx: Int;
|
||||
var tilesy: Int;
|
||||
var tilesFramerate: Int;
|
||||
|
||||
var count = 0;
|
||||
var lap = 0;
|
||||
var lapTime = 0.0;
|
||||
var m = Mat4.identity();
|
||||
|
||||
var ownerLoc = new Vec4();
|
||||
var ownerRot = new Quat();
|
||||
var ownerScl = new Vec4();
|
||||
|
||||
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;
|
||||
speed = 0;
|
||||
particles = [];
|
||||
ready = false;
|
||||
|
||||
Data.getParticle(sceneName, pref.particle, function(b: ParticleData) {
|
||||
data = b;
|
||||
r = data.raw;
|
||||
var dyn: Null<Bool> = r.dynamic_emitter;
|
||||
var dynValue: Bool = true;
|
||||
if (dyn != null) {
|
||||
dynValue = dyn;
|
||||
}
|
||||
dynamicEmitter = dynValue;
|
||||
if (Scene.active.raw.gravity != null) {
|
||||
gx = Scene.active.raw.gravity[0] * r.weight_gravity;
|
||||
gy = Scene.active.raw.gravity[1] * r.weight_gravity;
|
||||
gz = Scene.active.raw.gravity[2] * r.weight_gravity;
|
||||
}
|
||||
else {
|
||||
gx = 0;
|
||||
gy = 0;
|
||||
gz = -9.81 * r.weight_gravity;
|
||||
}
|
||||
alignx = r.object_align_factor[0];
|
||||
aligny = r.object_align_factor[1];
|
||||
alignz = r.object_align_factor[2];
|
||||
looptime = (r.frame_end - r.frame_start) / frameRate;
|
||||
lifetime = r.lifetime / frameRate;
|
||||
animtime = r.loop ? looptime : looptime + lifetime;
|
||||
spawnRate = ((r.frame_end - r.frame_start) / r.count) / frameRate;
|
||||
|
||||
for (i in 0...r.count) {
|
||||
particles.push(new Particle(i));
|
||||
}
|
||||
|
||||
ready = true;
|
||||
if (r.auto_start){
|
||||
start();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function start() {
|
||||
if (r.is_unique) random = Math.random();
|
||||
lifetime = r.lifetime / frameRate;
|
||||
time = 0;
|
||||
lap = 0;
|
||||
lapTime = 0;
|
||||
speed = currentSpeed;
|
||||
lastSpawnedCount = 0;
|
||||
instancedData = null;
|
||||
}
|
||||
|
||||
public function pause() {
|
||||
speed = 0;
|
||||
}
|
||||
|
||||
public function resume() {
|
||||
lifetime = r.lifetime / frameRate;
|
||||
speed = currentSpeed;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// TODO: interrupt smoothly
|
||||
public function stop() {
|
||||
end();
|
||||
}
|
||||
|
||||
function end() {
|
||||
lifetime = 0;
|
||||
speed = 0;
|
||||
lap = 0;
|
||||
}
|
||||
|
||||
public function update(object: MeshObject, owner: MeshObject) {
|
||||
if (!ready || object == null || speed == 0.0) return;
|
||||
if (iron.App.pauseUpdates) return;
|
||||
|
||||
var prevLap = lap;
|
||||
|
||||
// Copy owner world transform but discard scale
|
||||
owner.transform.world.decompose(ownerLoc, ownerRot, ownerScl);
|
||||
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);
|
||||
|
||||
object.transform.buildMatrix();
|
||||
owner.transform.buildMatrix();
|
||||
object.transform.dim.setFrom(owner.transform.dim);
|
||||
|
||||
dimx = object.transform.dim.x;
|
||||
dimy = object.transform.dim.y;
|
||||
|
||||
if (object.activeTilesheet != null) {
|
||||
tilesx = object.activeTilesheet.raw.tilesx;
|
||||
tilesy = object.activeTilesheet.raw.tilesy;
|
||||
tilesFramerate = object.activeTilesheet.raw.framerate;
|
||||
}
|
||||
|
||||
// Animate
|
||||
time += Time.renderDelta * speed; // realDelta to renderDelta
|
||||
lap = Std.int(time / animtime);
|
||||
lapTime = time - lap * animtime;
|
||||
count = Std.int(lapTime / spawnRate);
|
||||
|
||||
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;
|
||||
// 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;
|
||||
m._10 = hair ? 0 : alignx;
|
||||
m._11 = hair ? 0 : aligny;
|
||||
m._12 = hair ? 0 : alignz;
|
||||
m._13 = hair ? 0 : r.factor_random;
|
||||
m._20 = hair ? 0 : gx;
|
||||
m._21 = hair ? 0 : gy;
|
||||
m._22 = hair ? 0 : gz;
|
||||
m._23 = hair ? 0 : r.lifetime_random;
|
||||
m._30 = tilesx;
|
||||
m._31 = tilesy;
|
||||
m._32 = 1 / tilesFramerate;
|
||||
m._33 = hair ? 1 : lapTime;
|
||||
return m;
|
||||
}
|
||||
|
||||
public function getSizeRandom(): FastFloat {
|
||||
return r.size_random;
|
||||
}
|
||||
|
||||
public inline function getRandom(): FastFloat {
|
||||
return random;
|
||||
}
|
||||
|
||||
public inline function getSize(): FastFloat {
|
||||
return r.particle_size;
|
||||
}
|
||||
|
||||
function updateGpu(object: MeshObject, owner: MeshObject) {
|
||||
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) {
|
||||
var instancedData = new Float32Array(particles.length * 3);
|
||||
var i = 0;
|
||||
|
||||
var normFactor = 1 / 32767; // pa.values are not normalized
|
||||
var scalePosOwner = owner.data.scalePos;
|
||||
var scalePosParticle = object.data.scalePos;
|
||||
var particleSize = r.particle_size;
|
||||
var scaleFactor = new Vec4().setFrom(owner.transform.scale);
|
||||
scaleFactor.mult(scalePosOwner / (particleSize * scalePosParticle));
|
||||
|
||||
switch (r.emit_from) {
|
||||
case 0: // Vert
|
||||
var pa = owner.data.geom.positions;
|
||||
|
||||
for (p in particles) {
|
||||
var j = Std.int(fhash(i) * (pa.values.length / pa.size));
|
||||
instancedData.set(i, pa.values[j * pa.size ] * normFactor * scaleFactor.x); i++;
|
||||
instancedData.set(i, pa.values[j * pa.size + 1] * normFactor * scaleFactor.y); i++;
|
||||
instancedData.set(i, pa.values[j * pa.size + 2] * normFactor * scaleFactor.z); i++;
|
||||
}
|
||||
|
||||
case 1: // Face
|
||||
var positions = owner.data.geom.positions.values;
|
||||
|
||||
for (p in particles) {
|
||||
// Choose random index array (there is one per material) and random face
|
||||
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 v0 = new Vec3(positions[i0 * 4], positions[i0 * 4 + 1], positions[i0 * 4 + 2]);
|
||||
var v1 = new Vec3(positions[i1 * 4], positions[i1 * 4 + 1], positions[i1 * 4 + 2]);
|
||||
var v2 = new Vec3(positions[i2 * 4], positions[i2 * 4 + 1], positions[i2 * 4 + 2]);
|
||||
|
||||
var pos = randomPointInTriangle(v0, v1, v2);
|
||||
|
||||
instancedData.set(i, pos.x * normFactor * scaleFactor.x); i++;
|
||||
instancedData.set(i, pos.y * normFactor * scaleFactor.y); i++;
|
||||
instancedData.set(i, pos.z * normFactor * scaleFactor.z); i++;
|
||||
}
|
||||
|
||||
case 2: // Volume
|
||||
var scaleFactorVolume = new Vec4().setFrom(object.transform.dim);
|
||||
scaleFactorVolume.mult(0.5 / (particleSize * scalePosParticle));
|
||||
|
||||
for (p in particles) {
|
||||
instancedData.set(i, (Math.random() * 2.0 - 1.0) * scaleFactorVolume.x); i++;
|
||||
instancedData.set(i, (Math.random() * 2.0 - 1.0) * scaleFactorVolume.y); i++;
|
||||
instancedData.set(i, (Math.random() * 2.0 - 1.0) * scaleFactorVolume.z); i++;
|
||||
}
|
||||
}
|
||||
object.data.geom.setupInstanced(instancedData, 1, Usage.StaticUsage);
|
||||
}
|
||||
|
||||
// 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.
|
||||
**/
|
||||
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)));
|
||||
}
|
||||
}
|
||||
|
||||
class Particle {
|
||||
public var i: Int;
|
||||
|
||||
public var x = 0.0;
|
||||
public var y = 0.0;
|
||||
public var z = 0.0;
|
||||
|
||||
public var cameraDistance: Float;
|
||||
|
||||
public function new(i: Int) {
|
||||
this.i = i;
|
||||
}
|
||||
}
|
||||
|
||||
#if lnx_gpu_particles
|
||||
typedef ParticleSystem = ParticleSystemGPU;
|
||||
#elseif lnx_cpu_particles
|
||||
typedef ParticleSystem = ParticleSystemCPU;
|
||||
#else
|
||||
class ParticleSystem { public function new() { } }
|
||||
#end
|
||||
|
||||
551
leenkx/Sources/iron/object/ParticleSystemCPU.hx
Normal file
551
leenkx/Sources/iron/object/ParticleSystemCPU.hx
Normal file
@ -0,0 +1,551 @@
|
||||
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
|
||||
306
leenkx/Sources/iron/object/ParticleSystemGPU.hx
Normal file
306
leenkx/Sources/iron/object/ParticleSystemGPU.hx
Normal file
@ -0,0 +1,306 @@
|
||||
package iron.object;
|
||||
|
||||
#if lnx_gpu_particles
|
||||
import kha.FastFloat;
|
||||
import kha.graphics4.Usage;
|
||||
import kha.arrays.Float32Array;
|
||||
import iron.data.Data;
|
||||
import iron.data.ParticleData;
|
||||
import iron.data.SceneFormat;
|
||||
import iron.system.Time;
|
||||
import iron.math.Mat4;
|
||||
import iron.math.Quat;
|
||||
import iron.math.Vec3;
|
||||
import iron.math.Vec4;
|
||||
|
||||
class ParticleSystemGPU {
|
||||
public var data: ParticleData;
|
||||
public var speed = 1.0;
|
||||
var currentSpeed = 0.0;
|
||||
var particles: Array<Particle>;
|
||||
var ready: Bool;
|
||||
var frameRate = 24;
|
||||
var lifetime = 0.0;
|
||||
var looptime = 0.0;
|
||||
var animtime = 0.0;
|
||||
var time = 0.0;
|
||||
var spawnRate = 0.0;
|
||||
var seed = 0;
|
||||
|
||||
var r: TParticleData;
|
||||
var gx: Float;
|
||||
var gy: Float;
|
||||
var gz: Float;
|
||||
var alignx: Float;
|
||||
var aligny: Float;
|
||||
var alignz: Float;
|
||||
var dimx: Float;
|
||||
var dimy: Float;
|
||||
var tilesx: Int;
|
||||
var tilesy: Int;
|
||||
var tilesFramerate: Int;
|
||||
|
||||
var count = 0;
|
||||
var lap = 0;
|
||||
var lapTime = 0.0;
|
||||
var m = Mat4.identity();
|
||||
|
||||
var owner: MeshObject;
|
||||
var ownerLoc = new Vec4();
|
||||
var ownerRot = new Quat();
|
||||
var ownerScl = new Vec4();
|
||||
|
||||
var random = 0.0;
|
||||
|
||||
public function new(sceneName: String, pref: TParticleReference, mo: MeshObject) {
|
||||
seed = pref.seed;
|
||||
currentSpeed = speed;
|
||||
speed = 0;
|
||||
particles = [];
|
||||
ready = false;
|
||||
|
||||
Data.getParticle(sceneName, pref.particle, function(b: ParticleData) {
|
||||
data = b;
|
||||
r = data.raw;
|
||||
owner = mo;
|
||||
|
||||
if (Scene.active.raw.gravity != null) {
|
||||
gx = Scene.active.raw.gravity[0] * r.weight_gravity;
|
||||
gy = Scene.active.raw.gravity[1] * r.weight_gravity;
|
||||
gz = Scene.active.raw.gravity[2] * r.weight_gravity;
|
||||
}
|
||||
else {
|
||||
gx = 0;
|
||||
gy = 0;
|
||||
gz = -9.81 * r.weight_gravity;
|
||||
}
|
||||
|
||||
alignx = r.object_align_factor[0];
|
||||
aligny = r.object_align_factor[1];
|
||||
alignz = r.object_align_factor[2];
|
||||
|
||||
looptime = (r.frame_end - r.frame_start) / frameRate;
|
||||
lifetime = r.lifetime / frameRate;
|
||||
animtime = r.loop ? looptime : looptime + lifetime;
|
||||
spawnRate = ((r.frame_end - r.frame_start) / r.count) / frameRate;
|
||||
|
||||
for (i in 0...r.count) particles.push(new Particle(i));
|
||||
ready = true;
|
||||
|
||||
if (r.auto_start) start();
|
||||
});
|
||||
}
|
||||
|
||||
public function start() {
|
||||
if (r.is_unique) random = Math.random();
|
||||
lifetime = r.lifetime / frameRate;
|
||||
time = 0;
|
||||
lap = 0;
|
||||
lapTime = 0;
|
||||
speed = currentSpeed;
|
||||
}
|
||||
|
||||
public function pause() {
|
||||
speed = 0;
|
||||
}
|
||||
|
||||
public function resume() {
|
||||
lifetime = r.lifetime / frameRate;
|
||||
speed = currentSpeed;
|
||||
}
|
||||
|
||||
// TODO: interrupt smoothly
|
||||
public function stop() {
|
||||
end();
|
||||
}
|
||||
|
||||
function end() {
|
||||
lifetime = 0;
|
||||
speed = 0;
|
||||
lap = 0;
|
||||
}
|
||||
|
||||
public function update(object: MeshObject) {
|
||||
if (!ready || object == null || speed == 0.0) return;
|
||||
if (iron.App.pauseUpdates) return;
|
||||
|
||||
var prevLap = lap;
|
||||
|
||||
// Copy owner world transform but discard scale
|
||||
owner.transform.world.decompose(ownerLoc, ownerRot, ownerScl);
|
||||
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);
|
||||
|
||||
object.transform.buildMatrix();
|
||||
owner.transform.buildMatrix();
|
||||
object.transform.dim.setFrom(owner.transform.dim);
|
||||
|
||||
dimx = object.transform.dim.x;
|
||||
dimy = object.transform.dim.y;
|
||||
|
||||
if (object.tilesheet != null) {
|
||||
tilesx = object.tilesheet.getTilesX();
|
||||
tilesy = object.tilesheet.getTilesY();
|
||||
tilesFramerate = object.tilesheet.action.framerate;
|
||||
}
|
||||
|
||||
// Animate
|
||||
time += Time.renderDelta * speed;
|
||||
lap = Std.int(time / animtime);
|
||||
lapTime = time - lap * animtime;
|
||||
count = Std.int(lapTime / spawnRate);
|
||||
|
||||
if (lap > prevLap && !r.loop) {
|
||||
end();
|
||||
}
|
||||
|
||||
updateGpu(object);
|
||||
}
|
||||
|
||||
public function getData(): Mat4 {
|
||||
var hair = r.type == 1;
|
||||
m._00 = animtime;
|
||||
m._01 = hair ? 1 / particles.length : spawnRate;
|
||||
m._02 = hair ? 1 : lifetime;
|
||||
m._03 = particles.length;
|
||||
m._10 = hair ? 0 : alignx;
|
||||
m._11 = hair ? 0 : aligny;
|
||||
m._12 = hair ? 0 : alignz;
|
||||
m._13 = hair ? 0 : r.factor_random;
|
||||
m._20 = hair ? 0 : gx;
|
||||
m._21 = hair ? 0 : gy;
|
||||
m._22 = hair ? 0 : gz;
|
||||
m._23 = hair ? 0 : r.lifetime_random;
|
||||
m._30 = tilesx;
|
||||
m._31 = tilesy;
|
||||
m._32 = 1 / tilesFramerate;
|
||||
m._33 = hair ? 1 : lapTime;
|
||||
return m;
|
||||
}
|
||||
|
||||
public function getSizeRandom(): FastFloat {
|
||||
return r.size_random;
|
||||
}
|
||||
|
||||
public function getRandom(): FastFloat {
|
||||
return random;
|
||||
}
|
||||
|
||||
public function getSize(): FastFloat {
|
||||
return r.particle_size;
|
||||
}
|
||||
|
||||
function updateGpu(object: MeshObject) {
|
||||
if (!object.data.geom.instanced) setupGeomGpu(object);
|
||||
// GPU particles transform is attached to owner object
|
||||
}
|
||||
|
||||
function setupGeomGpu(object: MeshObject) {
|
||||
var instancedData = new Float32Array(particles.length * 3);
|
||||
var i = 0;
|
||||
|
||||
var normFactor = 1 / 32767; // pa.values are not normalized
|
||||
var scalePosOwner = owner.data.scalePos;
|
||||
var scalePosParticle = object.data.scalePos;
|
||||
var particleSize = r.particle_size;
|
||||
var scaleFactor = new Vec4().setFrom(owner.transform.scale);
|
||||
scaleFactor.mult(scalePosOwner / (particleSize * scalePosParticle));
|
||||
|
||||
switch (r.emit_from) {
|
||||
case 0: // Vert
|
||||
var pa = owner.data.geom.positions;
|
||||
|
||||
for (p in particles) {
|
||||
var j = Std.int(fhash(i) * (pa.values.length / pa.size));
|
||||
instancedData.set(i, pa.values[j * pa.size ] * normFactor * scaleFactor.x); i++;
|
||||
instancedData.set(i, pa.values[j * pa.size + 1] * normFactor * scaleFactor.y); i++;
|
||||
instancedData.set(i, pa.values[j * pa.size + 2] * normFactor * scaleFactor.z); i++;
|
||||
}
|
||||
|
||||
case 1: // Face
|
||||
var positions = owner.data.geom.positions.values;
|
||||
|
||||
for (p in particles) {
|
||||
// Choose random index array (there is one per material) and random face
|
||||
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 v0 = new Vec3(positions[i0 * 4], positions[i0 * 4 + 1], positions[i0 * 4 + 2]);
|
||||
var v1 = new Vec3(positions[i1 * 4], positions[i1 * 4 + 1], positions[i1 * 4 + 2]);
|
||||
var v2 = new Vec3(positions[i2 * 4], positions[i2 * 4 + 1], positions[i2 * 4 + 2]);
|
||||
|
||||
var pos = randomPointInTriangle(v0, v1, v2);
|
||||
|
||||
instancedData.set(i, pos.x * normFactor * scaleFactor.x); i++;
|
||||
instancedData.set(i, pos.y * normFactor * scaleFactor.y); i++;
|
||||
instancedData.set(i, pos.z * normFactor * scaleFactor.z); i++;
|
||||
}
|
||||
|
||||
case 2: // Volume
|
||||
var scaleFactorVolume = new Vec4().setFrom(object.transform.dim);
|
||||
scaleFactorVolume.mult(0.5 / (particleSize * scalePosParticle));
|
||||
|
||||
for (p in particles) {
|
||||
instancedData.set(i, (Math.random() * 2.0 - 1.0) * scaleFactorVolume.x); i++;
|
||||
instancedData.set(i, (Math.random() * 2.0 - 1.0) * scaleFactorVolume.y); i++;
|
||||
instancedData.set(i, (Math.random() * 2.0 - 1.0) * scaleFactorVolume.z); i++;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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.
|
||||
**/
|
||||
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)));
|
||||
}
|
||||
}
|
||||
|
||||
class Particle {
|
||||
public var i: Int;
|
||||
|
||||
public var x = 0.0;
|
||||
public var y = 0.0;
|
||||
public var z = 0.0;
|
||||
|
||||
public var cameraDistance: Float;
|
||||
|
||||
public function new(i: Int) {
|
||||
this.i = i;
|
||||
}
|
||||
}
|
||||
#end
|
||||
@ -1,53 +1,258 @@
|
||||
package iron.object;
|
||||
|
||||
import iron.Scene;
|
||||
import iron.data.Data;
|
||||
import iron.App;
|
||||
import iron.data.SceneFormat;
|
||||
import iron.system.Time;
|
||||
import haxe.ds.Map;
|
||||
|
||||
|
||||
@:allow(iron.Scene)
|
||||
class Tilesheet {
|
||||
|
||||
public var tileX = 0.0; // Tile offset on tilesheet texture 0-1
|
||||
public var tileY = 0.0;
|
||||
|
||||
public var raw: TTilesheetData;
|
||||
public var tileX: Float = 0.0;
|
||||
public var tileY: Float = 0.0;
|
||||
public var flipX: Bool = false;
|
||||
public var flipY: Bool = false;
|
||||
public var paused: Bool = false;
|
||||
public var frame: Int = 0;
|
||||
public var actions: Array<TTilesheetAction>;
|
||||
public var action: TTilesheetAction = null;
|
||||
var ready: Bool;
|
||||
|
||||
public var paused = false;
|
||||
public var frame = 0;
|
||||
var time = 0.0;
|
||||
public var ready: Bool = false;
|
||||
var time: Float = 0.0;
|
||||
var onActionComplete: Void->Void = null;
|
||||
var onReady: Void->Void = null;
|
||||
var onEvent: String->Void = null; // Callback for tilesheet events
|
||||
var prevFrame: Int = -1; // Track previous frame to detect changes
|
||||
var owner: MeshObject = null;
|
||||
var currentMesh: MeshObject = null;
|
||||
var meshCache: Map<String, MeshObject> = new Map();
|
||||
var pendingAction: String = null;
|
||||
var pendingOnComplete: Void->Void = null;
|
||||
|
||||
public function new(sceneName: String, tilesheet_ref: String, tilesheet_action_ref: String) {
|
||||
ready = false;
|
||||
Data.getSceneRaw(sceneName, function(format: TSceneFormat) {
|
||||
for (ts in format.tilesheet_datas) {
|
||||
if (ts.name == tilesheet_ref) {
|
||||
raw = ts;
|
||||
Scene.active.tilesheets.push(this);
|
||||
play(tilesheet_action_ref);
|
||||
ready = true;
|
||||
break;
|
||||
public function new(tilesheetData: TTilesheetData, ownerObject: MeshObject = null) {
|
||||
owner = ownerObject;
|
||||
actions = tilesheetData.actions;
|
||||
|
||||
pendingAction = tilesheetData.start_action;
|
||||
if ((pendingAction == null || pendingAction == "") && actions.length > 0) {
|
||||
pendingAction = actions[0].name;
|
||||
}
|
||||
|
||||
flipX = tilesheetData.flipx;
|
||||
flipY = tilesheetData.flipy;
|
||||
|
||||
// If no actions need mesh swapping, ready immediately
|
||||
var hasMeshActions: Bool = false;
|
||||
for (a in actions) {
|
||||
if (a.mesh != null && a.mesh != "") {
|
||||
hasMeshActions = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasMeshActions) {
|
||||
ready = true;
|
||||
if (pendingAction != null) {
|
||||
playAction(pendingAction);
|
||||
pendingAction = null;
|
||||
}
|
||||
if (onReady != null) onReady();
|
||||
}
|
||||
}
|
||||
|
||||
public function update() {
|
||||
if (App.pauseUpdates) return;
|
||||
|
||||
if (!ready) {
|
||||
if (tryInitialize()) {
|
||||
ready = true;
|
||||
if (pendingAction != null) {
|
||||
playAction(pendingAction, pendingOnComplete);
|
||||
pendingAction = null;
|
||||
pendingOnComplete = null;
|
||||
}
|
||||
if (onReady != null) onReady();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (paused || action == null || action.start >= action.end) return;
|
||||
|
||||
time += Time.renderDelta;
|
||||
|
||||
var frameTime = 1 / action.framerate;
|
||||
var framesToAdvance = 0;
|
||||
|
||||
while (time >= frameTime) {
|
||||
time -= frameTime;
|
||||
framesToAdvance++;
|
||||
}
|
||||
|
||||
if (framesToAdvance > 0) {
|
||||
setFrame(frame + framesToAdvance);
|
||||
}
|
||||
}
|
||||
|
||||
function tryInitialize(): Bool {
|
||||
if (owner == null) return false;
|
||||
|
||||
// If no children, use the owner mesh itself
|
||||
if (owner.children == null || owner.children.length == 0) {
|
||||
if (owner.data != null && !meshCache.exists(owner.data.name)) {
|
||||
meshCache.set(owner.data.name, owner);
|
||||
// Also cache by object name for flexible lookup
|
||||
if (owner.name != owner.data.name) {
|
||||
meshCache.set(owner.name, owner);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Use child meshes for mesh swapping
|
||||
for (child in owner.children) {
|
||||
if (Std.isOfType(child, MeshObject)) {
|
||||
var meshChild = cast(child, MeshObject);
|
||||
if (meshChild.data != null && !meshCache.exists(meshChild.data.name)) {
|
||||
meshCache.set(meshChild.data.name, meshChild);
|
||||
meshChild.visible = false;
|
||||
// Also cache by object name for flexible lookup
|
||||
if (meshChild.name != meshChild.data.name) {
|
||||
meshCache.set(meshChild.name, meshChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (a in actions) {
|
||||
if (a.mesh != null && a.mesh != "" && !meshCache.exists(a.mesh)) {
|
||||
if (findMatchingMesh(a.mesh) == null) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Find mesh by base name pattern (handles linked objects with different suffixes). */
|
||||
function findMatchingMesh(actionMeshName: String): MeshObject {
|
||||
var baseName = actionMeshName;
|
||||
|
||||
// Strip "Mesh" prefix if present
|
||||
if (StringTools.startsWith(baseName, "Mesh")) {
|
||||
baseName = baseName.substr(4);
|
||||
}
|
||||
|
||||
// Strip suffix after underscore (e.g., "_character.blend")
|
||||
var idx = baseName.indexOf("_");
|
||||
if (idx > 0) baseName = baseName.substr(0, idx);
|
||||
|
||||
for (meshName in meshCache.keys()) {
|
||||
if (meshName.indexOf(baseName) != -1) {
|
||||
var mesh = meshCache.get(meshName);
|
||||
meshCache.set(actionMeshName, mesh); // Cache alias
|
||||
return mesh;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function play(action_ref: String, onActionComplete: Void->Void = null) {
|
||||
this.onActionComplete = onActionComplete;
|
||||
for (a in raw.actions) {
|
||||
if (actions == null) return;
|
||||
|
||||
if (!ready) {
|
||||
pendingAction = action_ref;
|
||||
pendingOnComplete = onActionComplete;
|
||||
return;
|
||||
}
|
||||
|
||||
playAction(action_ref, onActionComplete);
|
||||
}
|
||||
|
||||
public function notifyOnReady(callback: Void->Void) {
|
||||
onReady = callback;
|
||||
if (ready) onReady();
|
||||
}
|
||||
|
||||
public function notifyOnEvent(callback: String->Void) {
|
||||
onEvent = callback;
|
||||
}
|
||||
|
||||
function playAction(action_ref: String, onComplete: Void->Void = null) {
|
||||
if (action != null && action.name == action_ref) {
|
||||
paused = false;
|
||||
return;
|
||||
}
|
||||
|
||||
onActionComplete = onComplete;
|
||||
|
||||
for (a in actions) {
|
||||
if (a.name == action_ref) {
|
||||
action = a;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (action == null) return;
|
||||
|
||||
if (action.mesh != null && action.mesh != "") {
|
||||
var targetMesh = meshCache.get(action.mesh);
|
||||
if (targetMesh != null && targetMesh != currentMesh) {
|
||||
swapMesh(targetMesh);
|
||||
}
|
||||
}
|
||||
|
||||
prevFrame = -1; // Reset previous frame for new action
|
||||
setFrame(action.start);
|
||||
paused = false;
|
||||
time = 0.0;
|
||||
}
|
||||
|
||||
function swapMesh(meshObj: MeshObject) {
|
||||
if (owner == null || meshObj == null) return;
|
||||
currentMesh = meshObj;
|
||||
if (meshObj.data != null) owner.setData(meshObj.data);
|
||||
if (meshObj.materials != null) owner.materials = meshObj.materials;
|
||||
}
|
||||
|
||||
function setFrame(f: Int) {
|
||||
frame = f;
|
||||
|
||||
if (frame > action.end && action.start < action.end) {
|
||||
// Check for events on last frame before completing
|
||||
checkEvents(prevFrame, action.end);
|
||||
if (onActionComplete != null) onActionComplete();
|
||||
if (action.loop) {
|
||||
prevFrame = -1; // Reset for loop
|
||||
setFrame(action.start);
|
||||
} else {
|
||||
paused = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for events between previous frame and current frame
|
||||
checkEvents(prevFrame, frame);
|
||||
prevFrame = frame;
|
||||
|
||||
var tx = frame % action.tilesx;
|
||||
var ty = Std.int(frame / action.tilesx);
|
||||
tileX = tx / action.tilesx;
|
||||
tileY = ty / action.tilesy;
|
||||
}
|
||||
|
||||
/** Check and fire events for frames between fromFrame (exclusive) and toFrame (inclusive). */
|
||||
function checkEvents(fromFrame: Int, toFrame: Int) {
|
||||
if (onEvent == null || action == null || action.events == null) return;
|
||||
|
||||
// Convert to action-relative frame numbers
|
||||
var relativeFrom = fromFrame - action.start;
|
||||
var relativeTo = toFrame - action.start;
|
||||
|
||||
for (evt in action.events) {
|
||||
// Fire event if it falls in the range (fromFrame, toFrame]
|
||||
if (evt.frame > relativeFrom && evt.frame <= relativeTo) {
|
||||
onEvent(evt.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function pause() {
|
||||
paused = true;
|
||||
}
|
||||
@ -57,61 +262,33 @@ class Tilesheet {
|
||||
}
|
||||
|
||||
public function remove() {
|
||||
Scene.active.tilesheets.remove(this);
|
||||
ready = false;
|
||||
action = null;
|
||||
actions = null;
|
||||
owner = null;
|
||||
currentMesh = null;
|
||||
pendingAction = null;
|
||||
pendingOnComplete = null;
|
||||
onEvent = null;
|
||||
prevFrame = -1;
|
||||
meshCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the frame of the current active tilesheet action. Automatically un-pauses action.
|
||||
* @param frame Frame offset with 0 as the first frame of the active action.
|
||||
**/
|
||||
public function setFrameOffset(frame: Int) {
|
||||
if (action == null) return;
|
||||
setFrame(action.start + frame);
|
||||
paused = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current frame.
|
||||
* @return Frame offset with 0 as the first frame of the active action.
|
||||
*/
|
||||
public function getFrameOffset(): Int {
|
||||
return frame - action.start;
|
||||
return action != null ? frame - action.start : 0;
|
||||
}
|
||||
|
||||
function update() {
|
||||
if (!ready || paused || action.start >= action.end) return;
|
||||
|
||||
time += Time.renderDelta;
|
||||
|
||||
var frameTime = 1 / raw.framerate;
|
||||
var framesToAdvance = 0;
|
||||
|
||||
// Check how many animation frames passed during the last render frame
|
||||
// and catch up if required. The remaining `time` that couldn't fit in
|
||||
// another animation frame will be used in the next `update()`.
|
||||
while (time >= frameTime) {
|
||||
time -= frameTime;
|
||||
framesToAdvance++;
|
||||
}
|
||||
|
||||
if (framesToAdvance != 0) {
|
||||
setFrame(frame + framesToAdvance);
|
||||
}
|
||||
public function getTilesX(): Int {
|
||||
return action != null ? action.tilesx : 1;
|
||||
}
|
||||
|
||||
function setFrame(f: Int) {
|
||||
frame = f;
|
||||
|
||||
// Action end
|
||||
if (frame > action.end && action.start < action.end) {
|
||||
if (onActionComplete != null) onActionComplete();
|
||||
if (action.loop) setFrame(action.start);
|
||||
else paused = true;
|
||||
return;
|
||||
}
|
||||
|
||||
var tx = frame % raw.tilesx;
|
||||
var ty = Std.int(frame / raw.tilesx);
|
||||
tileX = tx * (1 / raw.tilesx);
|
||||
tileY = ty * (1 / raw.tilesy);
|
||||
public function getTilesY(): Int {
|
||||
return action != null ? action.tilesy : 1;
|
||||
}
|
||||
}
|
||||
|
||||
@ -286,7 +286,7 @@ class Transform {
|
||||
public function applyParentInverse() {
|
||||
var pt = object.parent.transform;
|
||||
pt.buildMatrix();
|
||||
temp.getInverse(pt.world);
|
||||
temp.getInverse(pt.local);
|
||||
this.local.multmat(temp);
|
||||
this.decompose();
|
||||
this.buildMatrix();
|
||||
@ -295,7 +295,7 @@ class Transform {
|
||||
public function applyParent() {
|
||||
var pt = object.parent.transform;
|
||||
pt.buildMatrix();
|
||||
this.local.multmat(pt.world);
|
||||
this.local.multmat(pt.local);
|
||||
this.decompose();
|
||||
this.buildMatrix();
|
||||
}
|
||||
|
||||
@ -681,7 +681,11 @@ class Uniforms {
|
||||
}
|
||||
#end
|
||||
case "_backgroundCol": {
|
||||
if (camera.data.raw.clear_color != null) helpVec.set(camera.data.raw.clear_color[0], camera.data.raw.clear_color[1], camera.data.raw.clear_color[2]);
|
||||
if (Scene.active.world != null) {
|
||||
var col = Scene.active.world.raw.background_color;
|
||||
helpVec.set(((col >> 16) & 0xff) / 255, ((col >> 8) & 0xff) / 255, (col & 0xff) / 255);
|
||||
}
|
||||
else if (camera.data.raw.clear_color != null) helpVec.set(camera.data.raw.clear_color[0], camera.data.raw.clear_color[1], camera.data.raw.clear_color[2]);
|
||||
v = helpVec;
|
||||
}
|
||||
case "_hosekSunDirection": {
|
||||
@ -1095,7 +1099,7 @@ class Uniforms {
|
||||
m = helpMat;
|
||||
}
|
||||
#end
|
||||
#if lnx_particles
|
||||
#if lnx_gpu_particles
|
||||
case "_particleData": {
|
||||
var mo = cast(object, MeshObject);
|
||||
if (mo.particleOwner != null && mo.particleOwner.particleSystems != null) {
|
||||
@ -1106,18 +1110,9 @@ class Uniforms {
|
||||
}
|
||||
|
||||
if (m == null) {
|
||||
#if lnx_spot
|
||||
if (c.link.startsWith("_biasLightWorldViewProjectionMatrixSpot")) {
|
||||
var light = getSpot(c.link.charCodeAt(c.link.length - 1) - "0".code);
|
||||
if (light != null) {
|
||||
object == null ? helpMat.setIdentity() : helpMat.setFrom(object.transform.worldUnpack);
|
||||
helpMat.multmat(light.VP);
|
||||
helpMat.multmat(biasMat);
|
||||
m = helpMat;
|
||||
}
|
||||
}
|
||||
#if (!lnx_clusters && lnx_spot)
|
||||
if (c.link.startsWith("_biasLightViewProjectionMatrixSpot")) {
|
||||
var light = getSpot(c.link.charCodeAt(c.link.length - 1) - "0".code);
|
||||
var light = getSpot(0);
|
||||
if (light != null) {
|
||||
helpMat.setFrom(light.VP);
|
||||
helpMat.multmat(biasMat);
|
||||
@ -1251,14 +1246,19 @@ class Uniforms {
|
||||
var vy: Null<kha.FastFloat> = null;
|
||||
switch (c.link) {
|
||||
case "_tilesheetOffset": {
|
||||
var ts = cast(object, MeshObject).activeTilesheet;
|
||||
var ts = cast(object, MeshObject).tilesheet;
|
||||
vx = ts.tileX;
|
||||
vy = ts.tileY;
|
||||
}
|
||||
case "_tilesheetFlip": {
|
||||
var ts = cast(object, MeshObject).tilesheet;
|
||||
vx = ts.flipX ? 1.0 : 0.0;
|
||||
vy = ts.flipY ? 1.0 : 0.0;
|
||||
}
|
||||
case "_tilesheetTiles": {
|
||||
var ts = cast(object, MeshObject).activeTilesheet;
|
||||
vx = ts.raw.tilesx;
|
||||
vy = ts.raw.tilesy;
|
||||
var ts = cast(object, MeshObject).tilesheet;
|
||||
vx = ts.getTilesX();
|
||||
vy = ts.getTilesY();
|
||||
}
|
||||
#if lnx_morph_target
|
||||
case "_morphScaleOffset": {
|
||||
@ -1306,7 +1306,7 @@ class Uniforms {
|
||||
case "_texUnpack": {
|
||||
f = texUnpack != null ? texUnpack : 1.0;
|
||||
}
|
||||
#if lnx_particles
|
||||
#if lnx_gpu_particles
|
||||
case "_particleSizeRandom": {
|
||||
var mo = cast(object, MeshObject);
|
||||
if (mo.particleOwner != null && mo.particleOwner.particleSystems != null) {
|
||||
|
||||
Reference in New Issue
Block a user