Repe [T3DU] and Moises Jpelaez updates

This commit is contained in:
2026-05-12 23:54:06 -07:00
parent 6b404f9da6
commit 39091e8db3
147 changed files with 5539 additions and 1750 deletions

View File

@ -24,11 +24,11 @@ class App {
public static var renderPathTime: Float;
public static var endFrameCallbacks: Array<Void->Void> = [];
#end
static var last = 0.0;
static var time = 0.0;
static var lastw = -1;
static var lasth = -1;
public static var onResize: Void->Void = null;
public static var onResize: Void->Void = null; // TODO: deprecate this. Use leenkx.system.Signal 'resized' instead.
public static var resized: leenkx.system.Signal = new leenkx.system.Signal(); // args: (w: Int, h: Int)
public static function init(done: Void->Void) {
new App(done);
@ -75,6 +75,7 @@ class App {
lasth = App.h();
}
if (lastw != App.w() || lasth != App.h()) {
resized.emit(App.w(), App.h());
if (onResize != null) onResize();
else {
if (Scene.active != null && Scene.active.camera != null) {
@ -93,7 +94,6 @@ class App {
Scene.active.updateFrame();
time += iron.system.Time.delta;
while (time >= iron.system.Time.fixedStep) {
@ -101,16 +101,19 @@ class App {
time -= iron.system.Time.fixedStep;
}
@:privateAccess iron.system.Time._fixedStepInterpolation = time / iron.system.Time.fixedStep;
var i = 0;
var l = traitUpdates.length;
while (i < l) {
if (traitInits.length > 0) {
for (f in traitInits) {
traitInits.length > 0 ? f() : break;
}
traitInits.splice(0, traitInits.length);
while (traitInits.length > 0) {
var f = traitInits.shift();
if (f != null) f();
}
// Re-check bounds after processing inits (scene switch may have removed traits)
if (i < traitUpdates.length) {
traitUpdates[i]();
}
traitUpdates[i]();
// Account for removed traits
l <= traitUpdates.length ? i++ : l = traitUpdates.length;
}
@ -146,11 +149,9 @@ class App {
startTime = kha.Scheduler.realTime();
#end
if (traitInits.length > 0) {
for (f in traitInits) {
traitInits.length > 0 ? f() : break;
}
traitInits.splice(0, traitInits.length);
while (traitInits.length > 0) {
var f = traitInits.shift();
if (f != null) f();
}
// skip for XR callback to handle rendering

View File

@ -25,7 +25,6 @@ import iron.math.Quat;
#end
class RenderPath {
public static var active: RenderPath;
#if lnx_vr
static var vrSimulateMode: Bool = false;
@ -446,7 +445,9 @@ class RenderPath {
public function clearTarget(colorFlag: Null<Int> = null, depthFlag: Null<Float> = null) {
if (colorFlag == -1) { // -1 == 0xffffffff
if (Scene.active.world != null) {
colorFlag = Scene.active.world.raw.background_color;
var col = Scene.active.world.raw.background_color;
var strength = Scene.active.world.probe != null ? Scene.active.world.probe.raw.strength : 1.0;
colorFlag = Color.fromFloats(((col >> 16) & 0xff) / 255 * strength, ((col >> 8) & 0xff) / 255 * strength, (col & 0xff) / 255 * strength);
}
else if (Scene.active.camera != null) {
var cc = Scene.active.camera.data.raw.clear_color;
@ -477,7 +478,8 @@ class RenderPath {
if (depthDiff != 0) return depthDiff;
#end
return a.cameraDistance >= b.cameraDistance ? 1 : -1;
if (a.cameraDistance == b.cameraDistance) return 0;
return a.cameraDistance > b.cameraDistance ? 1 : -1;
});
}
@ -491,7 +493,9 @@ class RenderPath {
if (a.data.sortingIndex != b.data.sortingIndex) {
return a.data.sortingIndex > b.data.sortingIndex ? 1 : -1;
}
return a.data.name >= b.data.name ? 1 : -1;
if (a.data.name == b.data.name) return 0;
return a.data.name > b.data.name ? 1 : -1;
});
}
@ -667,9 +671,11 @@ class RenderPath {
end();
}
#if (rp_voxels != "Off")
public function getComputeShader(handle: String): kha.compute.Shader {
return Reflect.field(kha.Shaders, handle + "_comp");
}
#end
#if lnx_vr
// blits to each eyes viewport in the XR framebuffer.
@ -1229,7 +1235,7 @@ class CachedShaderContext {
public function new() {}
}
@:enum abstract DrawOrder(Int) from Int {
enum abstract DrawOrder(Int) from Int {
var Distance = 0; // Early-z
var Index = 1; // Less state changes
// var Mix = 2; // Distance buckets sorted by shader

View File

@ -64,7 +64,6 @@ class Scene {
#end
public var empties: Array<Object>;
public var animations: Array<Animation>;
public var tilesheets: Array<Tilesheet>;
#if lnx_skin
public var armatures: Array<Armature>;
#end
@ -78,6 +77,9 @@ class Scene {
public var traitRemoves: Array<Void->Void> = [];
var initializing: Bool; // Is the scene in its initialization phase?
var spawnDepth: Int = 0; // Nested spawn counter (defer trait creation while > 0)
var spawning(get, never): Bool;
inline function get_spawning(): Bool return spawnDepth > 0;
public function new() {
uid = uidCounter++;
@ -101,7 +103,6 @@ class Scene {
#end
empties = [];
animations = [];
tilesheets = [];
#if lnx_skin
armatures = [];
#end
@ -124,9 +125,8 @@ class Scene {
// Startup scene
active.addScene(format.name, null, function(sceneObject: Object) {
for (object in sceneObject.getChildren(true)) {
createTraits(object.raw.traits, object);
}
// Create traits bottom-up (children first, then parents)
createTraitsBottomUp(sceneObject);
#if lnx_terrain
if (format.terrain_ref != null) {
@ -141,17 +141,29 @@ class Scene {
active.camera = active.getCamera(format.camera_ref);
active.sceneParent = sceneObject;
active.ready = true;
for (f in active.traitInits) f();
active.traitInits = [];
active.ready = true;
active.initializing = false;
done(sceneObject);
});
});
}
// Create traits in post-order (bottom-up): children's traits are created before parents.
// This ensures that when a parent's notifyOnInit runs, children are already initialized.
static function createTraitsBottomUp(object: Object) {
// First, recursively process all children
for (child in object.children) {
createTraitsBottomUp(child);
}
// Then create traits for this object
if (object.raw != null) {
createTraits(object.raw.traits, object);
}
}
#if lnx_patch
public static var getRenderPath: Void->RenderPath;
public static function patch() {
@ -212,6 +224,8 @@ class Scene {
Data.getSceneRaw(sceneName, function(format: TSceneFormat) {
Scene.create(format, function(o: Object) {
framePassed = true;
if (done != null) done(o);
#if (rp_background == "World")
@ -242,10 +256,6 @@ class Scene {
if (!ready || RenderPath.active == null) return;
framePassed = true;
for (tilesheet in tilesheets) {
tilesheet.update();
}
// Render probes
#if rp_probes
var activeCamera = camera;
@ -289,33 +299,39 @@ class Scene {
return root.children.length > 0 ? root.children[0].getTrait(c) : null;
}
// TODO: solve name referencing for linked objects
public function getMesh(name: String): MeshObject {
for (m in meshes) if (m.name == name) return m;
return null;
}
// TODO: solve name referencing for linked objects
public function getLight(name: String): LightObject {
for (l in lights) if (l.name == name) return l;
return null;
}
// TODO: solve name referencing for linked objects
public function getCamera(name: String): CameraObject {
for (c in cameras) if (c.name == name) return c;
return null;
}
#if lnx_audio
// TODO: solve name referencing for linked objects
public function getSpeaker(name: String): SpeakerObject {
for (s in speakers) if (s.name == name) return s;
return null;
}
#end
// TODO: solve name referencing for linked objects
public function getEmpty(name: String): Object {
for (e in empties) if (e.name == name) return e;
return null;
}
// TODO: solve name referencing for linked objects
public function getGroup(name: String): Array<Object> {
if (groups == null) groups = new Map();
var g = groups.get(name);
@ -391,6 +407,7 @@ class Scene {
#end
var objectsCount = getObjectsCount(format.objects);
spawnDepth++; // Defer trait creation until all objects are ready
function traverseObjects(parent: Object, objects: Array<TObj>, parentObject: TObj, done: Void->Void) {
if (objects == null) return;
for (i in 0...objects.length) {
@ -408,11 +425,16 @@ class Scene {
}
if (format.objects == null || format.objects.length == 0) {
spawnDepth--;
createTraits(format.traits, parent); // Scene traits
done(parent);
}
else {
traverseObjects(parent, format.objects, null, function() { // Scene objects
spawnDepth--;
if (!initializing) {
createTraitsBottomUp(parent);
}
createTraits(format.traits, parent); // Scene traits
done(parent);
});
@ -426,7 +448,7 @@ class Scene {
var result = objects.length;
for (o in objects) {
if (discardNoSpawn && o.spawn != null && o.spawn == false) continue; // Do not count children of non-spawned objects
if (o.children != null) result += getObjectsCount(o.children);
if (o.children != null) result += getObjectsCount(o.children, discardNoSpawn);
}
return result;
}
@ -440,11 +462,16 @@ class Scene {
@param srcRaw If not `null`, spawn the object from the given scene data instead of using the scene this function is called on. Useful to spawn objects from other scenes.
**/
public function spawnObject(name: String, parent: Null<Object>, done: Null<Object->Void>, spawnChildren = true, srcRaw: Null<TSceneFormat> = null) {
spawnObjectInternal(name, parent, done, spawnChildren, srcRaw, true);
}
function spawnObjectInternal(name: String, parent: Null<Object>, done: Null<Object->Void>, spawnChildren: Bool, srcRaw: Null<TSceneFormat>, createTraits: Bool) {
if (srcRaw == null) srcRaw = raw;
var objectsTraversed = 0;
var obj = getRawObjectByName(srcRaw, name);
var objectsCount = spawnChildren ? getObjectsCount([obj], false) : 1;
var rootId = -1;
spawnDepth++; // Defer trait creation until all objects are ready
function spawnObjectTree(obj: TObj, parent: Object, parentObject: TObj, done: Object->Void) {
createObject(obj, srcRaw, parent, parentObject, function(object: Object) {
if (rootId == -1) {
@ -453,20 +480,27 @@ class Scene {
if (spawnChildren && obj.children != null) {
for (child in obj.children) spawnObjectTree(child, object, obj, done);
}
if (++objectsTraversed == objectsCount && done != null) {
if (++objectsTraversed == objectsCount) {
// Retrieve the originally spawned object from the current
// child object to ensure done() is called with the right
// object
while (object.uid != rootId) {
object = object.parent;
}
done(object);
// Create traits bottom-up after all objects are ready
spawnDepth--;
if (createTraits) {
createTraitsBottomUp(object);
}
// Then call user callback
if (done != null) done(object);
}
});
}
spawnObjectTree(obj, parent, null, done);
}
// TODO: solve name referencing for linked objects
public function parseObject(sceneName: String, objectName: String, parent: Object, done: Object->Void) {
Data.getSceneRaw(sceneName, function(format: TSceneFormat) {
var o: TObj = getRawObjectByName(format, objectName);
@ -495,6 +529,10 @@ class Scene {
static function traverseObjs(children: Array<TObj>, name: String): TObj {
for (o in children) {
if (o.name == name) return o;
else if (o.filename != "") {
var n: String = name + "_" + o.filename;
if (o.name == n) return o;
}
if (o.children != null) {
var res = traverseObjs(o.children, name);
if (res != null) return res;
@ -592,7 +630,7 @@ class Scene {
else {
for (object_ref in object_refs) {
// Spawn top-level collection objects and their children
spawnObject(object_ref, groupOwner, function(spawnedObject: Object) {
spawnObjectInternal(object_ref, groupOwner, function(spawnedObject: Object) {
// Apply collection/group instance offset to all
// top-level parents of that group
if (!isObjectInGroup(groupRef, spawnedObject.parent, format)) {
@ -610,9 +648,10 @@ class Scene {
}
if (++spawned == object_refs.length) {
groupOwner.transform.reset();
groupOwner.transform.buildMatrix();
done();
}
}, true, format);
}, true, format, false);
}
}
}
@ -764,6 +803,7 @@ class Scene {
#end
}
// TODO: solve name referencing for linked objects
public function returnMeshObject(object_file: String, data_ref: String, sceneName: String, armature: #if lnx_skin Armature #else Null<Int> #end, materials: Vector<MaterialData>, parent: Object, parentObject: TObj, o: TObj, done: Object->Void) {
Data.getMesh(object_file, data_ref, function(mesh: MeshData) {
var object = addMeshObject(mesh, materials, parent);
@ -779,9 +819,9 @@ class Scene {
for (ref in o.particle_refs) cast(object, MeshObject).setupParticleSystem(sceneName, ref);
}
#end
// Attach tilesheet
if (o.tilesheet_ref != null) {
cast(object, MeshObject).setupTilesheet(sceneName, o.tilesheet_ref, o.tilesheet_action_ref);
// Attach tilesheet from embedded object data
if (o.tilesheet != null) {
cast(object, MeshObject).setupTilesheet(o.tilesheet);
}
if (o.camera_list != null){
@ -820,6 +860,7 @@ class Scene {
if (object != null) {
object.raw = o;
object.name = o.name;
if (o.filename != null) object.filename = o.filename;
if (o.visible != null) object.visible = o.visible;
if (o.visible_mesh != null) object.visibleMesh = o.visible_mesh;
if (o.visible_shadow != null) object.visibleShadow = o.visible_shadow;
@ -848,9 +889,9 @@ class Scene {
}
}
// If the scene is still initializing, traits will be created later
// If the scene is still initializing or spawning, traits will be created later
// to ensure that object references for trait properties are valid
if (!active.initializing) createTraits(o.traits, object);
if (!active.initializing && !active.spawning) createTraits(o.traits, object);
}
done(object);
}
@ -882,17 +923,15 @@ class Scene {
// Set trait properties
if (t.props != null) {
var traitFields = Type.getInstanceFields(Type.getClass(traitInst));
for (i in 0...Std.int(t.props.length / 3)) {
var pname: String = t.props[i * 3];
var ptype: String = t.props[i * 3 + 1];
var pval: Dynamic = t.props[i * 3 + 2];
if (traitFields.indexOf(pname) == -1) continue;
if (StringTools.endsWith(ptype, "Object") && pval != "" && pval != null) {
Reflect.setProperty(traitInst, pname, Scene.active.getChild(pval));
} else if (ptype == "TSceneFormat" && pval != "") {
Data.getSceneRaw(pval, function (r: TSceneFormat) {
Reflect.setProperty(traitInst, pname, r);
});
}
else {
switch (ptype) {

View File

@ -14,9 +14,9 @@ class Trait {
var _add: Array<Void->Void> = null;
var _init: Array<Void->Void> = null;
var _remove: Array<Void->Void> = null;
var _fixedUpdate: Array<Void->Void> = null;
var _update: Array<Void->Void> = null;
var _lateUpdate: Array<Void->Void> = null;
var _fixedUpdate: Array<Void->Void> = null;
var _render: Array<kha.graphics4.Graphics->Void> = null;
var _render2D: Array<kha.graphics2.Graphics->Void> = null;

View File

@ -416,7 +416,7 @@ class Data {
var loading = loadingBlobs.get(file); // Is already being loaded
if (loading != null) {
loading.push(done);
return;
//return;
}
loadingBlobs.set(file, [done]); // Start loading

View File

@ -31,7 +31,6 @@ typedef TSceneFormat = {
@:optional public var speaker_datas: Array<TSpeakerData>;
@:optional public var world_datas: Array<TWorldData>;
@:optional public var world_ref: String;
@:optional public var tilesheet_datas: Array<TTilesheetData>;
@:optional public var objects: Array<TObj>;
@:optional public var groups: Array<TGroup>;
@:optional public var gravity: Float32Array;
@ -169,6 +168,7 @@ typedef TShaderOverride = {
@:structInit class TShaderOverride {
#end
@:optional public var cull_mode: String;
@:optional public var compare_mode: String;
@:optional public var addressing: String;
@:optional public var filter: String;
@:optional public var shared_sampler: String;
@ -364,18 +364,6 @@ typedef TProbeData = {
@:optional public var radiance_mipmaps: Null<Int>;
}
#if js
typedef TTilesheetData = {
#else
@:structInit class TTilesheetData {
#end
public var name: String;
public var tilesx: Int;
public var tilesy: Int;
public var framerate: Int;
public var actions: Array<TTilesheetAction>;
}
#if js
typedef TTilesheetAction = {
#else
@ -385,6 +373,31 @@ typedef TTilesheetAction = {
public var start: Int;
public var end: Int;
public var loop: Bool;
public var tilesx: Int;
public var tilesy: Int;
public var framerate: Int;
@:optional public var mesh: String; // Optional mesh to swap to when playing this action
@:optional public var events: Array<TTilesheetEvent>; // Optional events triggered on specific frames
}
#if js
typedef TTilesheetEvent = {
#else
@:structInit class TTilesheetEvent {
#end
public var name: String; // Event name
public var frame: Int; // Frame number when event triggers
}
#if js
typedef TTilesheetData = {
#else
@:structInit class TTilesheetData {
#end
public var actions: Array<TTilesheetAction>;
public var start_action: String;
public var flipx: Bool;
public var flipy: Bool;
}
#if js
@ -392,26 +405,47 @@ typedef TParticleData = {
#else
@:structInit class TParticleData {
#end
// Format
public var fps: Int;
public var name: String;
public var type: Int; // 0 - Emitter, Hair
// Lnx
public var auto_start: Bool;
public var dynamic_emitter: Bool;
public var is_unique: Bool;
public var local_coords: Bool;
public var loop: Bool;
// Emission
public var count: Int;
// public var hair_length: FastFloat; TODO
public var frame_start: FastFloat;
public var frame_end: FastFloat;
public var lifetime: FastFloat;
public var lifetime_random: FastFloat;
public var emit_from: Int; // 0 - Vert, 1 - Face, 2 - Volume
// Velocity
public var object_align_factor: Float32Array;
public var factor_random: FastFloat;
// Rotation
public var use_rotations: Bool;
public var rotation_mode: Int; // 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
public var rotation_factor_random: Float;
public var phase_factor: Float;
public var phase_factor_random: Float;
public var use_dynamic_rotation: Bool;
// Physics
public var physics_type: Int; // 0 - No, 1 - Newton
public var mass: FastFloat;
// Render
public var particle_size: FastFloat; // Object scale
public var size_random: FastFloat; // Random scale
public var mass: FastFloat;
public var show_emitter: Bool;
public var instance_object: String; // Object reference
// Field Weights
public var weight_gravity: FastFloat;
public var weight_texture: FastFloat;
// Textures
public var texture_slots: Dynamic;
}
#if js
@ -433,6 +467,7 @@ typedef TObj = {
public var name: String;
public var data_ref: String;
public var transform: TTransform;
@:optional public var filename: String; // For objects instanced from external files
@:optional public var material_refs: Array<String>;
@:optional public var particle_refs: Array<TParticleReference>;
@:optional public var render_emitter: Bool;
@ -462,8 +497,7 @@ typedef TObj = {
@:optional public var mobile: Null<Bool>;
@:optional public var spawn: Null<Bool>; // Auto add object when creating scene
@:optional public var local_only: Null<Bool>; // Apply parent matrix
@:optional public var tilesheet_ref: String;
@:optional public var tilesheet_action_ref: String;
@:optional public var tilesheet: TTilesheetData; // Embedded tilesheet data
@:optional public var sampled: Null<Bool>; // Object action
@:optional public var is_ik_fk_only: Null<Bool>; // Bone IK or FK only
@:optional public var bone_layers: Array<Bool>; // Bone Layer

View File

@ -230,6 +230,9 @@ class ShaderContext {
if (overrideContext.cull_mode != null) {
pipeState.cullMode = getCullMode(overrideContext.cull_mode);
}
if (overrideContext.compare_mode != null) {
pipeState.depthMode = getCompareMode(overrideContext.compare_mode);
}
}
pipeState.compile();

View File

@ -114,11 +114,11 @@ class TerrainStream {
var rawmeshData: TMeshData = {
name: "Terrain",
sorting_index: 0,
vertex_arrays: [pos, nor, tex],
index_arrays: [ind],
scale_pos: scalePos,
scale_tex: 1.0
scale_tex: 1.0,
sorting_index: 0
};
new MeshData(rawmeshData, function(data: MeshData) {

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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) {

View File

@ -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

View File

@ -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

View File

@ -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;
}

View File

@ -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

View 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

View 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

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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) {

View File

@ -601,8 +601,10 @@ class Keyboard extends VirtualInput {
function downListener(code: KeyCode) {
var s = keyCode(code);
keysFrame.push(s);
keysStarted.set(s, true);
if (!keysDown.get(s)) {
keysFrame.push(s);
keysStarted.set(s, true);
}
keysDown.set(s, true);
repeatTime = kha.Scheduler.time() + 0.4;
@ -618,8 +620,10 @@ class Keyboard extends VirtualInput {
function upListener(code: KeyCode) {
var s = keyCode(code);
keysFrame.push(s);
keysReleased.set(s, true);
if (keysDown.get(s)) {
keysFrame.push(s);
keysReleased.set(s, true);
}
keysDown.set(s, false);
#if kha_android_rmb
@ -746,7 +750,11 @@ class Gamepad extends VirtualInput {
}
else if (axis == 1 || axis == 3) { // Y
stick.lastY = stick.y;
#if (kha_html5 || lnx_debug_html5)
stick.y = -value;
#else
stick.y = value;
#end
stick.movementY = stick.y - stick.lastY;
}
stick.moved = true;
@ -765,13 +773,12 @@ class Gamepad extends VirtualInput {
}
class Sensor {
public var x = 0.0;
public var y = 0.0;
public var z = 0.0;
public function new() {
kha.input.Sensor.get(kha.input.SensorType.Accelerometer).notify(listener);
public function new(sensorType: kha.input.SensorType = kha.input.SensorType.Accelerometer) {
kha.input.Sensor.get(sensorType).notify(listener);
}
function listener(x: Float, y: Float, z: Float) {

View File

@ -111,12 +111,18 @@ class LnxPack {
#if js
var out = {};
#else
var out = Type.createEmptyInstance(getClass(key, parentKey));
var cls = getClass(key, parentKey);
var out: Dynamic = cls != null ? Type.createEmptyInstance(cls) : {};
var fields: Array<String> = cls != null ? Type.getInstanceFields(cls) : null;
#end
for (n in 0...length) {
var k = Std.string(read(i));
var raw = read(i);
var k = Std.string(raw);
var v = read(i, k, key);
Reflect.setField(out, k, v);
#if !js
if (fields == null || fields.indexOf(k) != -1)
#end
Reflect.setField(out, k, v);
}
return out;
}
@ -161,7 +167,9 @@ class LnxPack {
case "tracks": TTrack;
case "morph_target": TMorphTarget;
case "vertex_groups": TVertex_groups;
case _: TSceneFormat;
case "tilesheet": TTilesheetData;
case "events": TTilesheetEvent;
case _: null;
}
}
#end

View File

@ -2,27 +2,19 @@ package iron.system;
class Time {
public static var scale = 1.0;
// TODO: VR Frame Time Override - used to sync physics with VR headset refresh rate
#if lnx_vr
public static var vrFrameTime: Float = -1.0; // VR frame time in seconds (-1 = not in VR)
static var lastVRFrameTime: Float = 0.0;
static var vrFrameCount: Int = 0;
static var normalModeLogged: Bool = false;
#end
static var frequency: Null<Int> = null;
static function initFrequency() {
frequency = kha.Display.primary != null ? kha.Display.primary.frequency : 60;
}
public static var step(get, never): Float;
static function get_step(): Float {
if (frequency == null) initFrequency();
return 1 / frequency;
}
static var _fixedStep: Null<Float> = 1/60;
static var _fixedStep: Null<Float> = 1 / 60;
public static var fixedStep(get, never): Float;
static function get_fixedStep(): Float {
return _fixedStep;
@ -32,31 +24,45 @@ class Time {
_fixedStep = value;
}
static var _fixedStepInterpolation: Float = 0.0;
public static var fixedStepInterpolation(get, never): Float;
static function get_fixedStepInterpolation(): Float {
return _fixedStepInterpolation;
}
// TODO: VR Frame Time Override - used to sync physics with VR headset refresh rate
#if lnx_vr
public static var vrFrameTime: Float = -1.0; // VR frame time in seconds (-1 = not in VR)
static var lastVRFrameTime: Float = 0.0;
static var vrFrameCount: Int = 0;
static var normalModeLogged: Bool = false;
#end
static var lastTime = 0.0;
static var _delta = 0.0;
public static var delta(get, never): Float;
static function get_delta(): Float {
return _delta;
}
static var lastRenderTime = 0.0;
static var _renderDelta = 0.0;
public static var renderDelta(get, never): Float;
static function get_renderDelta(): Float {
return _renderDelta;
}
public static inline function time(): Float {
return kha.Scheduler.time() * scale;
return kha.Scheduler.time();
}
public static inline function realTime(): Float {
return kha.Scheduler.realTime() * scale;
return kha.Scheduler.realTime();
}
public static function update() {
#if lnx_vr
// TODO: use VR frame time when in VR present mode to sync physics with headset refresh
// TODO: use VR frame time when in VR present mode to sync physics with VR headset refresh
if (vrFrameTime >= 0.0) {
if (lastVRFrameTime > 0.0) {
_delta = vrFrameTime - lastVRFrameTime;

View File

@ -255,7 +255,7 @@ typedef TAnim = {
@:optional var _normalize: Array<Bool>;
}
@:enum abstract Ease(Int) from Int to Int {
enum abstract Ease(Int) from Int to Int {
var Linear = 0;
var SineIn = 1;
var SineOut = 2;