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

@ -39,6 +39,8 @@
-D lnx_skin -D lnx_skin
-D lnx_morph_target -D lnx_morph_target
-D lnx_particles -D lnx_particles
-D lnx_cpu_particles
-D lnx_gpu_particles
-D sys_krom -D sys_krom
-D sys_g1 -D sys_g1
-D sys_g2 -D sys_g2

View File

@ -6,8 +6,8 @@ bl_info = {
"location": "Properties -> Render -> Leenkx Player", "location": "Properties -> Render -> Leenkx Player",
"description": "Full Stack SDK", "description": "Full Stack SDK",
"author": "Leenkx.com", "author": "Leenkx.com",
"version": (1, 0, 8), "version": (2026, 5, 0),
"blender": (4, 2, 1), "blender": (4, 5, 0),
"doc_url": "https://leenkx.com/", "doc_url": "https://leenkx.com/",
"tracker_url": "https://leenkx.com/support" "tracker_url": "https://leenkx.com/support"
} }
@ -318,7 +318,7 @@ class LeenkxAddonPreferences(AddonPreferences):
layout.label(text="Welcome to Leenkx!") layout.label(text="Welcome to Leenkx!")
# Compare version Blender and Leenkx (major, minor) # Compare version Blender and Leenkx (major, minor)
if bpy.app.version[:2] not in [(4, 4), (4, 2), (3, 6), (3, 3)]: if bpy.app.version[:2] not in [(4, 5), (4, 4), (4, 2), (3, 6), (3, 3)]:
box = layout.box().column() box = layout.box().column()
box.label(text="Warning: For Leenkx to work correctly, use a Blender LTS version") box.label(text="Warning: For Leenkx to work correctly, use a Blender LTS version")
@ -550,13 +550,13 @@ def apply_unix_permissions(sdk):
os.path.join(sdk, "nodejs/node-osx"), os.path.join(sdk, "nodejs/node-osx"),
os.path.join(sdk, "Krom/Krom.app/Contents/MacOS/Krom"), os.path.join(sdk, "Krom/Krom.app/Contents/MacOS/Krom"),
# Kha tools # Kha tools
os.path.join(sdk, "Kha/Tools/macos/haxe"), os.path.join(sdk, "Kha/Tools/macos_x64/haxe"),
os.path.join(sdk, "Kha/Tools/macos/lame"), os.path.join(sdk, "Kha/Tools/macos_x64/lame"),
os.path.join(sdk, "Kha/Tools/macos/oggenc"), os.path.join(sdk, "Kha/Tools/macos_x64/oggenc"),
# Kinc tools # Kinc tools
os.path.join(sdk, "Kha/Kinc/Tools/macos/kmake"), os.path.join(sdk, "Kha/Kinc/Tools/macos_x64/kmake"),
os.path.join(sdk, "Kha/Kinc/Tools/macos/kraffiti"), os.path.join(sdk, "Kha/Kinc/Tools/macos_x64/kraffiti"),
os.path.join(sdk, "Kha/Kinc/Tools/macos/krafix"), os.path.join(sdk, "Kha/Kinc/Tools/macos_x64/krafix"),
] ]
for path in paths: for path in paths:
os.chmod(path, 0o777) os.chmod(path, 0o777)

View File

@ -37,7 +37,7 @@ float d_ggx(const float nh, const float a) {
vec3 specularBRDF(const vec3 f0, const float roughness, const float nl, const float nh, const float nv, const float vh) { vec3 specularBRDF(const vec3 f0, const float roughness, const float nl, const float nh, const float nv, const float vh) {
float a = roughness * roughness; float a = roughness * roughness;
vec3 result = d_ggx(nh, a) * g2_approx(nl, nv, a) * f_schlick(f0, vh) / max(4.0 * nv, 1e-5); //NdotL cancels out later vec3 result = d_ggx(nh, a) * g2_approx(nl, nv, a) * f_schlick(f0, vh) / max(4.0 * nv, 1e-5); //NdotL cancels out later
return min(result, vec3(200.0)); return result;
} }
// John Hable - Optimizing GGX Shaders // John Hable - Optimizing GGX Shaders
@ -79,7 +79,7 @@ vec3 orenNayarDiffuseBRDF(const vec3 albedo, const float roughness, const float
} }
vec3 lambertDiffuseBRDF(const vec3 albedo, const float nl) { vec3 lambertDiffuseBRDF(const vec3 albedo, const float nl) {
return albedo * nl; return albedo * (1.0 / 3.1415926535) * nl;
} }
vec3 surfaceAlbedo(const vec3 baseColor, const float metalness) { vec3 surfaceAlbedo(const vec3 baseColor, const float metalness) {

View File

@ -148,9 +148,10 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co
vec3(1.0, 0.0, t.y), vec3(1.0, 0.0, t.y),
vec3(0.0, t.z, 0.0), vec3(0.0, t.z, 0.0),
vec3(t.w, 0.0, t.x)); vec3(t.w, 0.0, t.x));
float ltcspec = ltcEvaluate(n, v, dotNV, p, invM, lightArea0, lightArea1, lightArea2, lightArea3); const float PI = 3.1415926535;
float ltcspec = ltcEvaluate(n, v, dotNV, p, invM, lightArea0, lightArea1, lightArea2, lightArea3) / PI;
ltcspec *= textureLod(sltcMag, tuv, 0.0).a; ltcspec *= textureLod(sltcMag, tuv, 0.0).a;
float ltcdiff = ltcEvaluate(n, v, dotNV, p, mat3(1.0), lightArea0, lightArea1, lightArea2, lightArea3); float ltcdiff = ltcEvaluate(n, v, dotNV, p, mat3(1.0), lightArea0, lightArea1, lightArea2, lightArea3) / PI;
vec3 direct = albedo * ltcdiff + ltcspec * spec * 0.05; vec3 direct = albedo * ltcdiff + ltcspec * spec * 0.05;
#else #else
vec3 direct = lambertDiffuseBRDF(albedo, dotNL) + vec3 direct = lambertDiffuseBRDF(albedo, dotNL) +
@ -181,7 +182,7 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co
#ifdef _ShadowMap #ifdef _ShadowMap
if (receiveShadow) { if (receiveShadow) {
#ifdef _SinglePoint #ifdef _SinglePoint
vec4 lPos = LWVPSpot[0] * vec4(p + n * bias * 2, 1.0); vec4 lPos = LWVPSpot[0] * vec4(p + n * bias * 10, 1.0);
direct *= shadowTest(shadowMapSpot[0], direct *= shadowTest(shadowMapSpot[0],
#ifdef _ShadowMapTransparent #ifdef _ShadowMapTransparent
shadowMapSpotTransparent[0], shadowMapSpotTransparent[0],
@ -193,7 +194,7 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co
); );
#endif #endif
#ifdef _Clusters #ifdef _Clusters
vec4 lPos = LWVPSpot[index] * vec4(p + n * bias * 2, 1.0); vec4 lPos = LWVPSpot[index] * vec4(p + n * bias * 10, 1.0);
if (index == 0) direct *= shadowTest(shadowMapSpot[0], if (index == 0) direct *= shadowTest(shadowMapSpot[0],
#ifdef _ShadowMapTransparent #ifdef _ShadowMapTransparent
shadowMapSpotTransparent[0], shadowMapSpotTransparent[0],
@ -435,9 +436,10 @@ vec3 sampleLightVoxels(const vec3 p, const vec3 n, const vec3 v, const float dot
vec3(1.0, 0.0, t.y), vec3(1.0, 0.0, t.y),
vec3(0.0, t.z, 0.0), vec3(0.0, t.z, 0.0),
vec3(t.w, 0.0, t.x)); vec3(t.w, 0.0, t.x));
float ltcspec = ltcEvaluate(n, v, dotNV, p, invM, lightArea0, lightArea1, lightArea2, lightArea3); const float PI = 3.1415926535;
float ltcspec = ltcEvaluate(n, v, dotNV, p, invM, lightArea0, lightArea1, lightArea2, lightArea3) / PI;
ltcspec *= textureLod(sltcMag, tuv, 0.0).a; ltcspec *= textureLod(sltcMag, tuv, 0.0).a;
float ltcdiff = ltcEvaluate(n, v, dotNV, p, mat3(1.0), lightArea0, lightArea1, lightArea2, lightArea3); float ltcdiff = ltcEvaluate(n, v, dotNV, p, mat3(1.0), lightArea0, lightArea1, lightArea2, lightArea3) / PI;
vec3 direct = albedo * ltcdiff + ltcspec * spec * 0.05; vec3 direct = albedo * ltcdiff + ltcspec * spec * 0.05;
#else #else
vec3 direct = lambertDiffuseBRDF(albedo, dotNL) + vec3 direct = lambertDiffuseBRDF(albedo, dotNL) +

View File

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

View File

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

View File

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

View File

@ -14,9 +14,9 @@ class Trait {
var _add: Array<Void->Void> = null; var _add: Array<Void->Void> = null;
var _init: Array<Void->Void> = null; var _init: Array<Void->Void> = null;
var _remove: Array<Void->Void> = null; var _remove: Array<Void->Void> = null;
var _fixedUpdate: Array<Void->Void> = null;
var _update: Array<Void->Void> = null; var _update: Array<Void->Void> = null;
var _lateUpdate: 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 _render: Array<kha.graphics4.Graphics->Void> = null;
var _render2D: Array<kha.graphics2.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 var loading = loadingBlobs.get(file); // Is already being loaded
if (loading != null) { if (loading != null) {
loading.push(done); loading.push(done);
return; //return;
} }
loadingBlobs.set(file, [done]); // Start loading loadingBlobs.set(file, [done]); // Start loading

View File

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

View File

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

View File

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

View File

@ -14,7 +14,7 @@ class Animation {
public var isSkinned: Bool; public var isSkinned: Bool;
public var isSampled: Bool; public var isSampled: Bool;
public var action = ""; @:isVar public var action(get, default) = "";
#if lnx_skin #if lnx_skin
public var armature: iron.data.Armature; // Bone public var armature: iron.data.Armature; // Bone
#end #end
@ -57,6 +57,10 @@ class Animation {
play(); play();
} }
function get_action(): String {
return action;
}
public function play(action = "", onComplete: Void->Void = null, blendTime = 0.0, speed = 1.0, loop = true) { public function play(action = "", onComplete: Void->Void = null, blendTime = 0.0, speed = 1.0, loop = true) {
if (blendTime > 0) { if (blendTime > 0) {
this.blendTime = blendTime; this.blendTime = blendTime;
@ -98,7 +102,7 @@ class Animation {
else { else {
sampler.timeOld = sampler.time; sampler.timeOld = sampler.time;
sampler.offsetOld = sampler.offset; sampler.offsetOld = sampler.offset;
sampler.setTimeOnly(sampler.time + delta * sampler.speed); sampler.setTimeOnly(sampler.time + delta * sampler.speed * iron.system.Time.scale);
updateActionTrack(sampler); updateActionTrack(sampler);
} }
} }

View File

@ -14,6 +14,8 @@ import iron.data.Armature;
import iron.data.Data; import iron.data.Data;
import iron.math.Ray; import iron.math.Ray;
import StringTools;
class BoneAnimation extends Animation { class BoneAnimation extends Animation {
public static var skinMaxBones = 128; 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> { public function initMatsEmpty(): Array<Mat4> {
var mats = []; var mats = [];
@ -136,6 +147,15 @@ class BoneAnimation extends Animation {
} }
} }
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) @:access(iron.object.Transform)
function updateBoneChildren(bone: TObj, bm: Mat4) { function updateBoneChildren(bone: TObj, bm: Mat4) {
var ar = boneChildren.get(bone.name); 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) { override public function play(action = "", onComplete: Void->Void = null, blendTime = 0.2, speed = 1.0, loop = true) {
if (action != "") { var actionName: String = getName(action);
setAction(action); if (actionName != "") {
super.play(action, onComplete, blendTime, speed, loop); setAction(actionName);
var tempAnimParam = new ActionSampler(action); super.play(actionName, onComplete, blendTime, speed, loop);
var tempAnimParam = new ActionSampler(actionName);
registerAction("tempAction", tempAnimParam); registerAction("tempAction", tempAnimParam);
updateAnimation = function(mats){ updateAnimation = function(mats){
sampleAction(tempAnimParam, mats); sampleAction(tempAnimParam, mats);

View File

@ -370,8 +370,6 @@ class LightObject extends Object {
} }
#end // lnx_csm #end // lnx_csm
#if lnx_clusters
// Centralize discarding conditions when iterating over lights // Centralize discarding conditions when iterating over lights
// Important to avoid issues later with "misaligned" data in uniforms (lightsArray, clusterData, LWVPSpotArray) // Important to avoid issues later with "misaligned" data in uniforms (lightsArray, clusterData, LWVPSpotArray)
public inline static function discardLight(light: LightObject) { public inline static function discardLight(light: LightObject) {
@ -382,6 +380,8 @@ class LightObject extends Object {
return #if lnx_shadowmap_atlas light.culledLight || #end discardLight(light); return #if lnx_shadowmap_atlas light.culledLight || #end discardLight(light);
} }
#if lnx_clusters
#if (lnx_shadowmap_atlas && lnx_shadowmap_atlas_lod) #if (lnx_shadowmap_atlas && lnx_shadowmap_atlas_lod)
// Arbitrary function to map from [0-16] to [1.0-0.0] // Arbitrary function to map from [0-16] to [1.0-0.0]
public inline static function zToShadowMapScale(z: Int, max: Int): Float { public inline static function zToShadowMapScale(z: Int, max: Int): Float {
@ -422,7 +422,8 @@ class LightObject extends Object {
#if lnx_spot // Point lamps first #if lnx_spot // Point lamps first
lights.sort(function(a, b): Int { 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 #end
@ -494,6 +495,13 @@ class LightObject extends Object {
continue; continue;
} }
#end #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 // Mark affected clusters
for (z in minZ...maxZ + 1) { for (z in minZ...maxZ + 1) {
for (y in minY...maxY + 1) { for (y in minY...maxY + 1) {

View File

@ -18,17 +18,18 @@ class MeshObject extends Object {
public var depthRead(default, null) = false; public var depthRead(default, null) = false;
#if lnx_particles #if lnx_particles
public var particleSystems: Array<ParticleSystem> = null; // Particle owner 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 particleChildren: Array<MeshObject> = null;
public var particleOwner: MeshObject = null; // Particle object public var particleOwner: MeshObject = null; // Particle object
public var particleIndex = -1; public var particleIndex = -1;
public var render_emitter = true;
#end #end
public var cameraDistance: Float; public var cameraDistance: Float;
public var cameraList: Array<String> = null; public var cameraList: Array<String> = null;
public var screenSize = 0.0; public var screenSize = 0.0;
public var frustumCulling = true; public var frustumCulling = true;
public var activeTilesheet: Tilesheet = null; public var tilesheet: Tilesheet = null;
public var tilesheets: Array<Tilesheet> = null;
public var skip_context: String = null; // Do not draw this context public var skip_context: String = null; // Do not draw this context
public var force_context: String = null; // Draw only this context public var force_context: String = null; // Draw only this context
static var lastPipeline: PipelineState = null; static var lastPipeline: PipelineState = null;
@ -72,18 +73,22 @@ class MeshObject extends Object {
#if lnx_batch #if lnx_batch
Scene.active.meshBatch.removeMesh(this); Scene.active.meshBatch.removeMesh(this);
#end #end
#if lnx_particles #if lnx_gpu_particles
if (particleChildren != null) { if (particleChildren != null) {
for (c in particleChildren) c.remove(); for (c in particleChildren) c.remove();
particleChildren = null; particleChildren = null;
} }
#end
#if lnx_particles
if (particleSystems != null) { if (particleSystems != null) {
for (psys in particleSystems) psys.remove(); for (psys in particleSystems) {
#if lnx_cpu_particles psys.stop(); #end
psys.remove();
}
particleSystems = null; particleSystems = null;
} }
#end #end
if (activeTilesheet != null) activeTilesheet.remove(); if (tilesheet != null) tilesheet.remove();
if (tilesheets != null) for (ts in tilesheets) { ts.remove(); }
if (Scene.active != null) Scene.active.meshes.remove(this); if (Scene.active != null) Scene.active.meshes.remove(this);
data.refcount--; data.refcount--;
super.remove(); super.remove();
@ -113,36 +118,20 @@ class MeshObject extends Object {
#if lnx_particles #if lnx_particles
public function setupParticleSystem(sceneName: String, pref: TParticleReference) { public function setupParticleSystem(sceneName: String, pref: TParticleReference) {
if (particleSystems == null) particleSystems = []; if (particleSystems == null) particleSystems = [];
var psys = new ParticleSystem(sceneName, pref); var psys = new ParticleSystem(sceneName, pref, this);
particleSystems.push(psys); particleSystems.push(psys);
} }
#end #end
public function setupTilesheet(sceneName: String, tilesheet_ref: String, tilesheet_action_ref: String) { public function setupTilesheet(tilesheetData: iron.data.SceneFormat.TTilesheetData) {
activeTilesheet = new Tilesheet(sceneName, tilesheet_ref, tilesheet_action_ref); tilesheet = new Tilesheet(tilesheetData, this);
if(tilesheets == null) tilesheets = new Array<Tilesheet>();
tilesheets.push(activeTilesheet);
} }
public function setActiveTilesheet(sceneName: String, tilesheet_ref: String, tilesheet_action_ref: String) { public function setTilesheetAction(actionRef: String) {
var set = false; if (tilesheet != null) {
// Check if tilesheet already created tilesheet.play(actionRef);
if (tilesheets != null) {
for (ts in tilesheets) {
if (ts.raw.name == tilesheet_ref) {
activeTilesheet = ts;
activeTilesheet.play(tilesheet_action_ref);
set = true;
break;
} }
} }
}
// If not already created
if (!set) {
setupTilesheet(sceneName, tilesheet_ref, tilesheet_action_ref);
}
}
inline function isLodMaterial(): Bool { inline function isLodMaterial(): Bool {
return (raw != null && raw.lod_material != null && raw.lod_material == true); return (raw != null && raw.lod_material != null && raw.lod_material == true);
@ -179,7 +168,7 @@ class MeshObject extends Object {
// Scale radius for skinned mesh and particle system // Scale radius for skinned mesh and particle system
// TODO: define skin & particle bounds // TODO: define skin & particle bounds
var radiusScale = data.isSkinned ? 2.0 : 1.0; var radiusScale = data.isSkinned ? 2.0 : 1.0;
#if lnx_particles #if lnx_gpu_particles
// particleSystems for update, particleOwner for render // particleSystems for update, particleOwner for render
if (particleSystems != null || particleOwner != null) radiusScale *= 1000; if (particleSystems != null || particleOwner != null) radiusScale *= 1000;
#end #end
@ -236,9 +225,14 @@ class MeshObject extends Object {
if (cullMesh(context, Scene.active.camera, RenderPath.active.light)) return; if (cullMesh(context, Scene.active.camera, RenderPath.active.light)) return;
var meshContext = raw != null ? context == "mesh" : false; 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 (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 (raw != null && raw.is_particle && particleOwner == null) return; // Instancing not yet set-up by particle system owner
if (particleSystems != null && meshContext) { if (particleSystems != null && meshContext) {
if (particleChildren == null) { if (particleChildren == null) {
@ -257,9 +251,11 @@ class MeshObject extends Object {
} }
} }
for (i in 0...particleSystems.length) { 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 && particleSystems.length > 0 && !render_emitter) return;
if (particleSystems == null && cullMaterial(context)) return; if (particleSystems == null && cullMaterial(context)) return;
#else #else

View File

@ -11,6 +11,7 @@ class Object {
public var raw: TObj = null; public var raw: TObj = null;
public var name: String = ""; public var name: String = "";
public var filename: String = "";
public var transform: Transform; public var transform: Transform;
public var constraints: Array<Constraint> = null; public var constraints: Array<Constraint> = null;
public var traits: Array<Trait> = []; public var traits: Array<Trait> = [];
@ -111,12 +112,15 @@ class Object {
**/ **/
public function getChild(name: String): Object { public function getChild(name: String): Object {
if (this.name == name) return this; if (this.name == name) return this;
else { else if (this.filename != "") {
if (this.name == name + "_" + this.filename) return this;
}
for (c in children) { for (c in children) {
var r = c.getChild(name); var r = c.getChild(name);
if (r != null) return r; if (r != null) return r;
} }
}
return null; return null;
} }
@ -209,6 +213,16 @@ class Object {
return null; 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 #if lnx_skin
public function getBoneAnimation(armatureUid: Int): BoneAnimation { public function getBoneAnimation(armatureUid: Int): BoneAnimation {
for (a in Scene.active.animations) { for (a in Scene.active.animations) {
@ -218,10 +232,21 @@ class Object {
} }
return null; 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 #else
public function getBoneAnimation(armatureUid): Animation { public function getBoneAnimation(armatureUid): Animation {
return null; return null;
} }
public function getParentArmature(name: String): Animation {
return null;
}
#end #end
public function getObjectAnimation(): ObjectAnimation { public function getObjectAnimation(): ObjectAnimation {
@ -229,6 +254,15 @@ class Object {
return null; 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) { public function setupAnimation(oactions: Array<TSceneFormat> = null) {
// Parented to bone // Parented to bone
#if lnx_skin #if lnx_skin

View File

@ -9,6 +9,7 @@ import iron.math.Vec4;
import iron.math.Mat4; import iron.math.Mat4;
import iron.math.Quat; import iron.math.Quat;
import iron.data.SceneFormat; import iron.data.SceneFormat;
import StringTools;
class ObjectAnimation extends Animation { class ObjectAnimation extends Animation {
@ -42,13 +43,24 @@ class ObjectAnimation extends Animation {
isSkinned = false; isSkinned = false;
super(); 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 { function getAction(action: String): TObj {
for (a in oactions) if (a != null && a.objects[0].name == action) return a.objects[0]; for (a in oactions) if (a != null && a.objects[0].name == action) return a.objects[0];
return null; return null;
} }
override public function play(action = "", onComplete: Void->Void = null, blendTime = 0.0, speed = 1.0, loop = true) { 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){ if (this.action == "" && oactions != null && oactions[0] != null){
this.action = oactions[0].objects[0].name; this.action = oactions[0].objects[0].name;
} }

View File

@ -1,461 +1,9 @@
package iron.object; package iron.object;
#if lnx_particles #if lnx_gpu_particles
typedef ParticleSystem = ParticleSystemGPU;
import kha.FastFloat; #elseif lnx_cpu_particles
import kha.graphics4.Usage; typedef ParticleSystem = ParticleSystemCPU;
import kha.arrays.Float32Array; #else
import iron.data.Data; class ParticleSystem { public function new() { } }
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;
}
}
#end #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; package iron.object;
import iron.Scene; import iron.App;
import iron.data.Data;
import iron.data.SceneFormat; import iron.data.SceneFormat;
import iron.system.Time; import iron.system.Time;
import haxe.ds.Map;
@:allow(iron.Scene)
class Tilesheet { class Tilesheet {
public var tileX = 0.0; // Tile offset on tilesheet texture 0-1 public var tileX: Float = 0.0;
public var tileY = 0.0; public var tileY: Float = 0.0;
public var flipX: Bool = false;
public var raw: TTilesheetData; 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; public var action: TTilesheetAction = null;
var ready: Bool;
public var paused = false; public var ready: Bool = false;
public var frame = 0; var time: Float = 0.0;
var time = 0.0;
var onActionComplete: Void->Void = null; 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) { public function new(tilesheetData: TTilesheetData, ownerObject: MeshObject = null) {
ready = false; owner = ownerObject;
Data.getSceneRaw(sceneName, function(format: TSceneFormat) { actions = tilesheetData.actions;
for (ts in format.tilesheet_datas) {
if (ts.name == tilesheet_ref) { pendingAction = tilesheetData.start_action;
raw = ts; if ((pendingAction == null || pendingAction == "") && actions.length > 0) {
Scene.active.tilesheets.push(this); pendingAction = actions[0].name;
play(tilesheet_action_ref); }
ready = true;
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; 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) { public function play(action_ref: String, onActionComplete: Void->Void = null) {
this.onActionComplete = onActionComplete; if (actions == null) return;
for (a in raw.actions) {
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) { if (a.name == action_ref) {
action = a; action = a;
break; 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); setFrame(action.start);
paused = false; paused = false;
time = 0.0; 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() { public function pause() {
paused = true; paused = true;
} }
@ -57,61 +262,33 @@ class Tilesheet {
} }
public function remove() { 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) { public function setFrameOffset(frame: Int) {
if (action == null) return;
setFrame(action.start + frame); setFrame(action.start + frame);
paused = false; paused = false;
} }
/**
* Returns the current frame.
* @return Frame offset with 0 as the first frame of the active action.
*/
public function getFrameOffset(): Int { public function getFrameOffset(): Int {
return frame - action.start; return action != null ? frame - action.start : 0;
} }
function update() { public function getTilesX(): Int {
if (!ready || paused || action.start >= action.end) return; return action != null ? action.tilesx : 1;
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) { public function getTilesY(): Int {
setFrame(frame + framesToAdvance); return action != null ? action.tilesy : 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);
} }
} }

View File

@ -286,7 +286,7 @@ class Transform {
public function applyParentInverse() { public function applyParentInverse() {
var pt = object.parent.transform; var pt = object.parent.transform;
pt.buildMatrix(); pt.buildMatrix();
temp.getInverse(pt.world); temp.getInverse(pt.local);
this.local.multmat(temp); this.local.multmat(temp);
this.decompose(); this.decompose();
this.buildMatrix(); this.buildMatrix();
@ -295,7 +295,7 @@ class Transform {
public function applyParent() { public function applyParent() {
var pt = object.parent.transform; var pt = object.parent.transform;
pt.buildMatrix(); pt.buildMatrix();
this.local.multmat(pt.world); this.local.multmat(pt.local);
this.decompose(); this.decompose();
this.buildMatrix(); this.buildMatrix();
} }

View File

@ -681,7 +681,11 @@ class Uniforms {
} }
#end #end
case "_backgroundCol": { 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; v = helpVec;
} }
case "_hosekSunDirection": { case "_hosekSunDirection": {
@ -1095,7 +1099,7 @@ class Uniforms {
m = helpMat; m = helpMat;
} }
#end #end
#if lnx_particles #if lnx_gpu_particles
case "_particleData": { case "_particleData": {
var mo = cast(object, MeshObject); var mo = cast(object, MeshObject);
if (mo.particleOwner != null && mo.particleOwner.particleSystems != null) { if (mo.particleOwner != null && mo.particleOwner.particleSystems != null) {
@ -1106,18 +1110,9 @@ class Uniforms {
} }
if (m == null) { if (m == null) {
#if lnx_spot #if (!lnx_clusters && 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 (c.link.startsWith("_biasLightViewProjectionMatrixSpot")) { if (c.link.startsWith("_biasLightViewProjectionMatrixSpot")) {
var light = getSpot(c.link.charCodeAt(c.link.length - 1) - "0".code); var light = getSpot(0);
if (light != null) { if (light != null) {
helpMat.setFrom(light.VP); helpMat.setFrom(light.VP);
helpMat.multmat(biasMat); helpMat.multmat(biasMat);
@ -1251,14 +1246,19 @@ class Uniforms {
var vy: Null<kha.FastFloat> = null; var vy: Null<kha.FastFloat> = null;
switch (c.link) { switch (c.link) {
case "_tilesheetOffset": { case "_tilesheetOffset": {
var ts = cast(object, MeshObject).activeTilesheet; var ts = cast(object, MeshObject).tilesheet;
vx = ts.tileX; vx = ts.tileX;
vy = ts.tileY; 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": { case "_tilesheetTiles": {
var ts = cast(object, MeshObject).activeTilesheet; var ts = cast(object, MeshObject).tilesheet;
vx = ts.raw.tilesx; vx = ts.getTilesX();
vy = ts.raw.tilesy; vy = ts.getTilesY();
} }
#if lnx_morph_target #if lnx_morph_target
case "_morphScaleOffset": { case "_morphScaleOffset": {
@ -1306,7 +1306,7 @@ class Uniforms {
case "_texUnpack": { case "_texUnpack": {
f = texUnpack != null ? texUnpack : 1.0; f = texUnpack != null ? texUnpack : 1.0;
} }
#if lnx_particles #if lnx_gpu_particles
case "_particleSizeRandom": { case "_particleSizeRandom": {
var mo = cast(object, MeshObject); var mo = cast(object, MeshObject);
if (mo.particleOwner != null && mo.particleOwner.particleSystems != null) { if (mo.particleOwner != null && mo.particleOwner.particleSystems != null) {

View File

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

View File

@ -111,11 +111,17 @@ class LnxPack {
#if js #if js
var out = {}; var out = {};
#else #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 #end
for (n in 0...length) { 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); var v = read(i, k, key);
#if !js
if (fields == null || fields.indexOf(k) != -1)
#end
Reflect.setField(out, k, v); Reflect.setField(out, k, v);
} }
return out; return out;
@ -161,7 +167,9 @@ class LnxPack {
case "tracks": TTrack; case "tracks": TTrack;
case "morph_target": TMorphTarget; case "morph_target": TMorphTarget;
case "vertex_groups": TVertex_groups; case "vertex_groups": TVertex_groups;
case _: TSceneFormat; case "tilesheet": TTilesheetData;
case "events": TTilesheetEvent;
case _: null;
} }
} }
#end #end

View File

@ -3,14 +3,6 @@ package iron.system;
class Time { class Time {
public static var scale = 1.0; 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 var frequency: Null<Int> = null;
static function initFrequency() { static function initFrequency() {
frequency = kha.Display.primary != null ? kha.Display.primary.frequency : 60; frequency = kha.Display.primary != null ? kha.Display.primary.frequency : 60;
@ -32,6 +24,20 @@ class Time {
_fixedStep = value; _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 lastTime = 0.0;
static var _delta = 0.0; static var _delta = 0.0;
public static var delta(get, never): Float; public static var delta(get, never): Float;
@ -47,16 +53,16 @@ class Time {
} }
public static inline function time(): Float { public static inline function time(): Float {
return kha.Scheduler.time() * scale; return kha.Scheduler.time();
} }
public static inline function realTime(): Float { public static inline function realTime(): Float {
return kha.Scheduler.realTime() * scale; return kha.Scheduler.realTime();
} }
public static function update() { public static function update() {
#if lnx_vr #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 (vrFrameTime >= 0.0) {
if (lastVRFrameTime > 0.0) { if (lastVRFrameTime > 0.0) {
_delta = vrFrameTime - lastVRFrameTime; _delta = vrFrameTime - lastVRFrameTime;

View File

@ -255,7 +255,7 @@ typedef TAnim = {
@:optional var _normalize: Array<Bool>; @: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 Linear = 0;
var SineIn = 1; var SineIn = 1;
var SineOut = 2; var SineOut = 2;

View File

@ -13,7 +13,7 @@ class AddParticleToObjectNode extends LogicNode {
} }
override function run(from: Int) { override function run(from: Int) {
#if lnx_particles #if lnx_gpu_particles
if (property0 == 'Scene Active'){ if (property0 == 'Scene Active'){
var objFrom: Object = inputs[1].get(); var objFrom: Object = inputs[1].get();
@ -47,7 +47,7 @@ class AddParticleToObjectNode extends LogicNode {
var oslot: Int = mobjTo.particleSystems.length-1; var oslot: Int = mobjTo.particleSystems.length-1;
var opsys = mobjTo.particleSystems[oslot]; var opsys = mobjTo.particleSystems[oslot];
@:privateAccess opsys.setupGeomGpu(mobjTo.particleChildren[oslot], mobjTo); @:privateAccess opsys.setupGeomGpu(mobjTo.particleChildren[oslot]);
} else { } else {
var sceneName: String = inputs[1].get(); var sceneName: String = inputs[1].get();
@ -82,7 +82,7 @@ class AddParticleToObjectNode extends LogicNode {
var oslot: Int = mobjTo.particleSystems.length-1; var oslot: Int = mobjTo.particleSystems.length-1;
var opsys = mobjTo.particleSystems[oslot]; var opsys = mobjTo.particleSystems[oslot];
@:privateAccess opsys.setupGeomGpu(mobjTo.particleChildren[oslot], mobjTo); @:privateAccess opsys.setupGeomGpu(mobjTo.particleChildren[oslot]);
break; break;
} }

View File

@ -4,6 +4,7 @@ import iron.object.Object;
#if lnx_physics #if lnx_physics
import leenkx.trait.physics.PhysicsConstraint; import leenkx.trait.physics.PhysicsConstraint;
import leenkx.trait.physics.PhysicsConstraint.ConstraintAxis;
#if lnx_bullet #if lnx_bullet
import leenkx.trait.physics.bullet.PhysicsConstraint.ConstraintType; import leenkx.trait.physics.bullet.PhysicsConstraint.ConstraintType;
#elseif lnx_jolt #elseif lnx_jolt
@ -31,7 +32,6 @@ class AddPhysicsConstraintNode extends LogicNode {
if (pivotObject == null || rb1 == null || rb2 == null) return; if (pivotObject == null || rb1 == null || rb2 == null) return;
#if lnx_physics #if lnx_physics
var disableCollisions: Bool = inputs[4].get(); var disableCollisions: Bool = inputs[4].get();
var breakable: Bool = inputs[5].get(); var breakable: Bool = inputs[5].get();
var breakingThreshold: Float = inputs[6].get(); var breakingThreshold: Float = inputs[6].get();

View File

@ -31,7 +31,7 @@ class DrawImageNode extends LogicNode {
RenderToTexture.g.rotate(angle, x, y); RenderToTexture.g.rotate(angle, x, y);
if (imgName != lastImgName) { if (imgName != lastImgName || img == null) {
// Load new image // Load new image
lastImgName = imgName; lastImgName = imgName;
iron.data.Data.getImage(imgName, (image: Image) -> { iron.data.Data.getImage(imgName, (image: Image) -> {

View File

@ -0,0 +1,107 @@
package leenkx.logicnode;
import iron.math.Vec4;
import kha.Image;
import kha.Color;
import leenkx.renderpath.RenderToTexture;
class DrawImageRenderNode extends LogicNode {
var img: Image;
public function new(tree: LogicTree) {
super(tree);
}
override function run(from: Int) {
if (from == 1)
tree.notifyOnRender(render);
else {
RenderToTexture.ensure2DContext("DrawImageRenderNode");
final colorVec: Vec4 = inputs[3].get();
final anchorH: Int = inputs[4].get();
final anchorV: Int = inputs[5].get();
final x: Float = inputs[6].get();
final y: Float = inputs[7].get();
final width: Float = inputs[8].get();
final height: Float = inputs[9].get();
final sx: Float = inputs[10].get();
final sy: Float = inputs[11].get();
final swidth: Float = inputs[12].get();
final sheight: Float = inputs[13].get();
final angle: Float = inputs[14].get();
final drawx = x - 0.5 * width * anchorH;
final drawy = y - 0.5 * height * anchorV;
RenderToTexture.g.rotate(angle, x, y);
if (img != null){
RenderToTexture.g.color = 0xff000000;
RenderToTexture.g.fillRect(drawx, drawy, width, height);
RenderToTexture.g.color = RenderToTexture.g.color = Color.fromFloats(colorVec.x, colorVec.y, colorVec.z, colorVec.w);
RenderToTexture.g.drawScaledSubImage(img, sx, sy, swidth, sheight, drawx, drawy, width, height);
}
RenderToTexture.g.rotate(-angle, x, y);
runOutput(0);
}
}
function render(g: kha.graphics4.Graphics) {
var camera = inputs[2].get();
img = kha.Image.createRenderTarget(iron.App.w(), iron.App.h(),
kha.graphics4.TextureFormat.RGBA32,
kha.graphics4.DepthStencilFormat.NoDepthAndStencil);
final sceneCam = iron.Scene.active.camera;
final oldRT = camera.renderTarget;
iron.Scene.active.camera = camera;
camera.renderTarget = img;
camera.renderFrame(g);
img = camera.renderTarget;
if (inputs[15].get() || kha.Image.renderTargetsInvertedY()) {
img = kha.Image.createRenderTarget(iron.App.w(), iron.App.h(),
kha.graphics4.TextureFormat.RGBA32,
kha.graphics4.DepthStencilFormat.NoDepthAndStencil);
img.g2.begin(true, Color.Transparent);
img.g2.color = Color.White;
if (kha.Image.renderTargetsInvertedY()) {
img.g2.drawScaledImage(camera.renderTarget, 0, iron.App.h(), iron.App.w(), -iron.App.h());
} else {
img.g2.drawImage(camera.renderTarget, 0, 0);
}
if (inputs[15].get()) {
for (f in @:privateAccess iron.App.traitRenders2D) {
f(img.g2);
}
}
img.g2.end();
}
camera.renderTarget = oldRT;
iron.Scene.active.camera = sceneCam;
tree.removeRender(render);
}
}

View File

@ -14,7 +14,7 @@ class DrawSubImageNode extends LogicNode {
} }
override function run(from: Int) { override function run(from: Int) {
RenderToTexture.ensure2DContext("DrawImageNode"); RenderToTexture.ensure2DContext("DrawSubImageNode");
final imgName: String = inputs[1].get(); final imgName: String = inputs[1].get();
final colorVec: Vec4 = inputs[2].get(); final colorVec: Vec4 = inputs[2].get();
@ -32,12 +32,10 @@ class DrawSubImageNode extends LogicNode {
final drawx = x - 0.5 * width * anchorH; final drawx = x - 0.5 * width * anchorH;
final drawy = y - 0.5 * height * anchorV; final drawy = y - 0.5 * height * anchorV;
final sdrawx = sx - 0.5 * swidth * anchorH;
final sdrawy = sy - 0.5 * sheight * anchorV;
RenderToTexture.g.rotate(angle, x, y); RenderToTexture.g.rotate(angle, x, y);
if (imgName != lastImgName) { if (imgName != lastImgName || img == null) {
// Load new image // Load new image
lastImgName = imgName; lastImgName = imgName;
iron.data.Data.getImage(imgName, (image: Image) -> { iron.data.Data.getImage(imgName, (image: Image) -> {
@ -51,7 +49,7 @@ class DrawSubImageNode extends LogicNode {
} }
RenderToTexture.g.color = Color.fromFloats(colorVec.x, colorVec.y, colorVec.z, colorVec.w); RenderToTexture.g.color = Color.fromFloats(colorVec.x, colorVec.y, colorVec.z, colorVec.w);
RenderToTexture.g.drawScaledSubImage(img, sdrawx, sdrawy, swidth, sheight, drawx, drawy, width, height); RenderToTexture.g.drawScaledSubImage(img, sx, sy, swidth, sheight, drawx, drawy, width, height);
RenderToTexture.g.rotate(-angle, x, y); RenderToTexture.g.rotate(-angle, x, y);
runOutput(0); runOutput(0);

View File

@ -0,0 +1,71 @@
package leenkx.logicnode;
import kha.Color;
import iron.math.Vec4;
import leenkx.renderpath.RenderToTexture;
class DrawToImageNode extends LogicNode {
var img: kha.Image = null;
public function new(tree: LogicTree) {
super(tree);
}
override function run(from: Int) {
var file: String = inputs[1].get();
var colorVec: Vec4 = inputs[2].get();
img = kha.Image.createRenderTarget(inputs[3].get(), inputs[4].get(),
kha.graphics4.TextureFormat.RGBA32,
kha.graphics4.DepthStencilFormat.NoDepthAndStencil);
RenderToTexture.ensureEmptyRenderTarget("DrawToImageNode");
img.g2.begin();
RenderToTexture.g = img.g2;
RenderToTexture.g.color = Color.fromFloats(colorVec.x, colorVec.y, colorVec.z, colorVec.w);
RenderToTexture.g.fillRect(0, 0, img.width, img.height);
runOutput(0);
RenderToTexture.g = null;
img.g2.end();
var pixels = img.getPixels();
var tx = inputs[5].get();
var ty = inputs[6].get();
var tw = inputs[7].get();
var th = inputs[8].get();
var bo = new haxe.io.BytesOutput();
var rgb = haxe.io.Bytes.alloc(tw * th * 4);
for (j in ty...ty + th) {
for (i in tx...tx + tw) {
var l = j * img.width + i;
var m = (j - ty) * tw + i - tx;
//ARGB 0xff
rgb.set(m * 4 + 0, pixels.get(l * 4 + 3));
rgb.set(m * 4 + 1, pixels.get(l * 4 + 0));
rgb.set(m * 4 + 2, pixels.get(l * 4 + 1));
rgb.set(m * 4 + 3, pixels.get(l * 4 + 2));
}
}
var imgwriter = new iron.format.bmp.Writer(bo);
imgwriter.write(iron.format.bmp.Tools.buildFromARGB(tw, th, rgb));
#if kha_krom
Krom.fileSaveBytes(Krom.getFilesLocation() + "/" + file, bo.getBytes().getData());
#elseif kha_html5
var blob = new js.html.Blob([bo.getBytes().getData()], {type: "application"});
var url = js.html.URL.createObjectURL(blob);
var a = cast(js.Browser.document.createElement("a"), js.html.AnchorElement);
a.href = url;
a.download = file;
a.click();
js.html.URL.revokeObjectURL(url);
#end
}
}

View File

@ -0,0 +1,80 @@
package leenkx.logicnode;
import iron.math.Vec4;
import kha.Image;
import kha.Color;
import leenkx.renderpath.RenderToTexture;
class DrawToScreenNode extends LogicNode {
var img: Image;
var colorVec: Vec4;
var anchorH: Int;
var anchorV: Int;
var x: Float;
var y: Float;
var width: Float;
var height: Float;
var sx: Float;
var sy: Float;
var swidth: Float;
var sheight: Float;
var angle: Float;
var drawx: Float;
var drawy: Float;
public function new(tree: LogicTree) {
super(tree);
}
override function run(from: Int) {
if (from == 0){
if (img == null){
runOutput(0);
return;
}
colorVec = inputs[4].get();
anchorH = inputs[5].get();
anchorV = inputs[6].get();
x = inputs[7].get();
y = inputs[8].get();
width = inputs[9].get();
height = inputs[10].get();
sx = inputs[11].get();
sy = inputs[12].get();
swidth = inputs[13].get();
sheight = inputs[14].get();
angle = inputs[15].get();
drawx = x - 0.5 * width * anchorH;
drawy = y - 0.5 * height * anchorV;
RenderToTexture.g.rotate(angle, x, y);
RenderToTexture.g.color = Color.fromFloats(colorVec.x, colorVec.y, colorVec.z, colorVec.w);
RenderToTexture.g.drawScaledSubImage(img, sx, sy, swidth, sheight, drawx, drawy, width, height);
RenderToTexture.g.rotate(-angle, x, y);
runOutput(0);
} else {
if (img == null)
img = kha.Image.createRenderTarget(inputs[2].get(), inputs[3].get(),
kha.graphics4.TextureFormat.RGBA32,
kha.graphics4.DepthStencilFormat.NoDepthAndStencil);
RenderToTexture.ensureEmptyRenderTarget("DrawToScreenNode");
img.g2.begin(inputs[16].get(), Color.Transparent);
RenderToTexture.g = img.g2;
runOutput(1);
RenderToTexture.g = null;
img.g2.end();
}
}
}

View File

@ -0,0 +1,23 @@
package leenkx.logicnode;
import leenkx.system.Signal;
class EmitSignalNode extends LogicNode {
public function new(tree: LogicTree) {
super(tree);
}
override function run(from: Int) {
var signal: Signal = inputs[1].get();
if (signal == null)
return;
var args: Array<Any> = [];
for (i in 2...inputs.length) {
args.push(inputs[i].get());
}
Reflect.callMethod(signal, Reflect.field(signal, "emit"), args);
runOutput(0);
}
}

View File

@ -0,0 +1,96 @@
package leenkx.logicnode;
import iron.math.Vec4;
import kha.Color;
import kha.Image;
class GetImageColorNode extends LogicNode {
public var property0: String;
var renderTarget: Image = null;
public function new(tree: LogicTree) {
super(tree);
renderTarget = Image.createRenderTarget(iron.App.w(), iron.App.h(), kha.graphics4.TextureFormat.RGBA32,
kha.graphics4.DepthStencilFormat.NoDepthAndStencil);
}
override function get(from: Int): Dynamic {
var i: Int;
var j: Int;
if (property0 == 'Image'){
i = inputs[1].get();
j = inputs[2].get();
} else {
i = inputs[0].get();
j = inputs[1].get();
}
if (i < 0 || j < 0) return null;
if (property0 != 'Image')
if (i > renderTarget.width || j > renderTarget.height) return null;
renderTarget.g2.begin(true, Color.Transparent);
renderTarget.g2.color = Color.White;
if (property0 == 'Render' || property0 == 'Render&Render2D'){
if (leenkx.renderpath.RenderPathCreator.finalTarget != null){
var img: Image = iron.RenderPath.active.renderTargets.get("buf").image;
renderTarget.g2.drawScaledImage(img, 0, 0, iron.App.w(), iron.App.h());
}
}
if (Image.renderTargetsInvertedY()){
renderTarget.g2.scale(1, -1);
renderTarget.g2.translate(0, renderTarget.height);
}
if (property0 == 'Image'){
var img: Image;
iron.data.Data.getImage(inputs[0].get(), (image: Image) -> {
img = image;
});
if (img == null || i > img.width || j > img.height){
renderTarget.g2.end();
return null;
} else
renderTarget.g2.drawScaledImage(img, 0, 0, img.width, img.height);
}
if (property0.indexOf('2D') > 0)
for (f in @:privateAccess iron.App.traitRenders2D)
f(renderTarget.g2);
if (Image.renderTargetsInvertedY()){
renderTarget.g2.scale(1, -1);
renderTarget.g2.translate(0, renderTarget.height);
}
renderTarget.g2.end();
var pixels = renderTarget.getPixels();
var k = j * renderTarget.width + i;
#if kha_krom
var l = k;
#elseif kha_html5
var l = (renderTarget.height - j) * renderTarget.width + i;
#end
var r = pixels.get(l * 4 + 0)/255;
var g = pixels.get(l * 4 + 1)/255;
var b = pixels.get(l * 4 + 2)/255;
var a = pixels.get(l * 4 + 3)/255;
var v = new Vec4(r, g, b, a);
return v;
}
}

View File

@ -14,7 +14,7 @@ class GetParticleDataNode extends LogicNode {
if (object == null) return null; if (object == null) return null;
#if lnx_particles #if lnx_gpu_particles
var mo = cast(object, iron.object.MeshObject); var mo = cast(object, iron.object.MeshObject);

View File

@ -13,7 +13,7 @@ class GetParticleNode extends LogicNode {
if (object == null) return null; if (object == null) return null;
#if lnx_particles #if lnx_gpu_particles
var mo = cast(object, iron.object.MeshObject); var mo = cast(object, iron.object.MeshObject);

View File

@ -0,0 +1,21 @@
package leenkx.logicnode;
import iron.object.MeshObject;
class GetTilesheetFlipNode extends LogicNode {
public function new(tree: LogicTree) {
super(tree);
}
override function get(from: Int): Dynamic {
var object: MeshObject = inputs[0].get();
if (object == null) return null;
if (object.tilesheet == null) return null;
if (from == 0) return object.tilesheet.flipX;
if (from == 1) return object.tilesheet.flipY;
return null;
}
}

View File

@ -11,13 +11,13 @@ class GetTilesheetStateNode extends LogicNode {
override function get(from: Int): Dynamic { override function get(from: Int): Dynamic {
var object: MeshObject = inputs[0].get(); var object: MeshObject = inputs[0].get();
if (object == null) return null; if (object == null || object.tilesheet == null) return null;
var tilesheet = object.activeTilesheet; var tilesheet = object.tilesheet;
return switch (from) { return switch (from) {
case 0: tilesheet.raw.name; case 0: object.name; // Return object name since tilesheet is embedded
case 1: tilesheet.action.name; case 1: tilesheet.action != null ? tilesheet.action.name : null;
case 2: tilesheet.getFrameOffset(); case 2: tilesheet.getFrameOffset();
case 3: tilesheet.frame; case 3: tilesheet.frame;
case 4: tilesheet.paused; case 4: tilesheet.paused;

View File

@ -0,0 +1,24 @@
package leenkx.logicnode;
import leenkx.system.Signal;
class GlobalSignalNode extends LogicNode {
public static var signals: Map<String, Signal> = new Map<String, Signal>();
public function new(tree: LogicTree) {
super(tree);
}
override function get(from: Int): Null<Signal> {
var name: String = inputs[0].get();
if (name == null || name == "")
return null;
var signal: Signal = signals.get(name);
if (signal == null) {
signal = new Signal();
signals.set(name, signal);
}
return signal;
}
}

View File

@ -33,6 +33,7 @@ class LogicTree extends iron.Trait {
if (paused) return; if (paused) return;
paused = true; paused = true;
if (_fixedUpdate != null) for (f in _fixedUpdate) iron.App.removeFixedUpdate(f);
if (_update != null) for (f in _update) iron.App.removeUpdate(f); if (_update != null) for (f in _update) iron.App.removeUpdate(f);
if (_lateUpdate != null) for (f in _lateUpdate) iron.App.removeLateUpdate(f); if (_lateUpdate != null) for (f in _lateUpdate) iron.App.removeLateUpdate(f);
} }
@ -41,6 +42,7 @@ class LogicTree extends iron.Trait {
if (!paused) return; if (!paused) return;
paused = false; paused = false;
if (_fixedUpdate != null) for (f in _fixedUpdate) iron.App.notifyOnFixedUpdate(f);
if (_update != null) for (f in _update) iron.App.notifyOnUpdate(f); if (_update != null) for (f in _update) iron.App.notifyOnUpdate(f);
if (_lateUpdate != null) for (f in _lateUpdate) iron.App.notifyOnLateUpdate(f); if (_lateUpdate != null) for (f in _lateUpdate) iron.App.notifyOnLateUpdate(f);
} }

View File

@ -11,10 +11,10 @@ class OnContactArrayNode extends LogicNode {
public function new(tree: LogicTree) { public function new(tree: LogicTree) {
super(tree); super(tree);
tree.notifyOnUpdate(update); tree.notifyOnFixedUpdate(fixedUpdate);
} }
function update() { function fixedUpdate() {
var object1: Object = inputs[0].get(); var object1: Object = inputs[0].get();
var objects: Array<Object> = inputs[1].get(); var objects: Array<Object> = inputs[1].get();

View File

@ -15,10 +15,10 @@ class OnContactNode extends LogicNode {
public function new(tree: LogicTree) { public function new(tree: LogicTree) {
super(tree); super(tree);
tree.notifyOnUpdate(update); tree.notifyOnFixedUpdate(fixedUpdate);
} }
function update() { function fixedUpdate() {
var object1: Object = inputs[0].get(); var object1: Object = inputs[0].get();
var object2: Object = inputs[1].get(); var object2: Object = inputs[1].get();

View File

@ -0,0 +1,49 @@
package leenkx.logicnode;
import leenkx.system.Signal;
class OnSignalNode extends LogicNode {
var emittedArgs: Array<Dynamic> = [];
var connectedSignal: Signal = null;
var callback: haxe.Constraints.Function = null;
public function new(tree: LogicTree) {
super(tree);
tree.notifyOnInit(init);
tree.notifyOnRemove(onRemove);
}
function init() {
var signal: Signal = inputs[0].get();
if (signal == null)
return;
connectedSignal = signal;
callback = onSignal;
signal.connect(callback);
}
function onSignal(...args: Any) {
emittedArgs = [];
for (arg in args) {
emittedArgs.push(arg);
}
runOutput(0);
}
override function get(from: Int): Any {
var argIndex: Int = from - 1;
if (argIndex >= 0 && argIndex < emittedArgs.length) {
return emittedArgs[argIndex];
}
return null;
}
function onRemove() {
if (connectedSignal != null && callback != null) {
connectedSignal.disconnect(callback);
connectedSignal = null;
callback = null;
}
}
}

View File

@ -4,7 +4,7 @@ import leenkx.trait.physics.PhysicsWorld;
class OnUpdateNode extends LogicNode { class OnUpdateNode extends LogicNode {
public var property0: String; // Update, Late Update, Physics Pre-Update public var property0: String; // Update, Fixed Update, Late Update, Physics Pre-Update
public function new(tree: LogicTree) { public function new(tree: LogicTree) {
super(tree); super(tree);
@ -13,6 +13,7 @@ class OnUpdateNode extends LogicNode {
function init() { function init() {
switch (property0) { switch (property0) {
case "Fixed Update": tree.notifyOnFixedUpdate(update);
case "Late Update": tree.notifyOnLateUpdate(update); case "Late Update": tree.notifyOnLateUpdate(update);
#if lnx_physics #if lnx_physics
case "Physics Pre-Update": PhysicsWorld.active.notifyOnPreUpdate(update); case "Physics Pre-Update": PhysicsWorld.active.notifyOnPreUpdate(update);

View File

@ -11,9 +11,9 @@ class PauseTilesheetNode extends LogicNode {
override function run(from: Int) { override function run(from: Int) {
var object: MeshObject = inputs[1].get(); var object: MeshObject = inputs[1].get();
if (object == null) return; if (object == null || object.tilesheet == null) return;
object.activeTilesheet.pause(); object.tilesheet.pause();
runOutput(0); runOutput(0);
} }

View File

@ -0,0 +1,210 @@
package leenkx.logicnode;
import iron.object.Animation;
import iron.object.Animation.ActionSampler;
import iron.object.Object;
import iron.Scene;
import kha.arrays.Float32Array;
import iron.object.ObjectAnimation;
#if lnx_skin
import iron.object.BoneAnimation;
#end
class PlayActionFromNode extends LogicNode {
var animation: Animation;
var startFrame: Int;
var endFrame: Int = -1;
var loop: Bool;
var reverse: Bool;
var action: String;
var actionR: String;
var sampler: ActionSampler;
public function new(tree: LogicTree) {
super(tree);
tree.notifyOnUpdate(update);
}
function update() {
if (sampler != null && action == sampler.action) {
if (sampler.offset == endFrame-1) {
if (loop) sampler.setFrameOffset(startFrame);
else {
if (!sampler.paused) {
sampler.paused = true;
runOutput(1);
}
}
}
}
}
override function run(from: Int) {
var object: Object = inputs[1].get();
action = inputs[2].get();
startFrame = inputs[3].get();
endFrame = inputs[4].get();
var blendTime: Float = inputs[5].get();
var speed: Float = inputs[6].get();
loop = inputs[7].get();
reverse = inputs[8].get();
if (object == null) return;
animation = object.animation;
if (animation == null) animation = object.getBoneAnimation(object.uid);
if (animation == null) return;
if (reverse){
var isnew = true;
actionR = action+'Reverse';
#if lnx_skin
if (animation.isSkinned){
for(a in animation.armature.actions)
if (a.name == actionR) isnew = false;
if (isnew){
for(a in animation.armature.actions)
if(a.name == action){
var bones = [];
var cn = [];
for(bone in a.bones){
var v = bone.anim.tracks[0];
var len: Int = v.values.length;
var val = new Float32Array(len);
var l = Std.int(len/16);
for(i in 0...l)
for(j in 0...16){
val[i*16+j] = v.values[(l-i)*16+j-16];
}
if (bone.children != null){
var cdn = [];
for (child in bone.children)
cdn.push(child.name);
cn.push(cdn);
} else cn.push(null);
var a: iron.data.SceneFormat.TObj = {
type : bone.type,
name : bone.name,
transform : {
values : bone.transform.values
},
anim : {
tracks : [{
target : v.target,
frames : v.frames,
values : val,
ref_values : null
}]
},
children : bone.children,
data_ref : null
}
if (bone.parent != null){
a.parent = bone.parent;
}
bones.push(a);
}
for (i in 0...bones.length){
var cd = [];
if (cn[i] != null)
for (name in cn[i])
for (bone in bones)
if (bone.name == name)
cd.push(bone);
bones[i].children = cd;
}
for (i in 0...bones.length){
if (bones[i].parent != null)
for (bone in bones)
if (bone.name == bones[i].parent.name)
bones[i].parent = bone;
}
animation.armature.actions.push({
name: actionR,
bones: bones,
mats: null});
var mats: Array<iron.math.Mat4> = [];
for (bone in a.bones) mats.push(iron.math.Mat4.fromFloat32Array(bone.transform.values));
var castBoneAnim = cast(animation, BoneAnimation);
castBoneAnim.data.geom.actions.set(actionR, bones);
castBoneAnim.data.geom.mats.set(actionR, mats);
for(o in iron.Scene.active.raw.objects)
if (o.name == object.name) o.bone_actions.push('action_'+o.bone_actions[0].split('_')[1]+'_'+actionR);
}
}
}
else
#end
{
var oaction = null;
var tracks: Array<iron.data.SceneFormat.TTrack> = [];
var oactions = cast(animation, ObjectAnimation).oactions;
for (a in oactions)
if (a != null && a.objects[0].name == actionR) isnew = false;
if (isnew){
for (a in oactions){
if (a != null && a.objects[0].name == action){
oaction = a.objects[0];
for(b in a.objects[0].anim.tracks){
var val: Array<Float> = [];
for(c in b.values) val.unshift(c);
var vali = new Float32Array(val.length);
for(i in 0...val.length) vali[i] = val[i];
tracks.push({target: b.target, frames: b.frames, values: vali});
}
oactions.push({
objects: [{name: actionR,
anim: {begin: oaction.anim.begin, end: oaction.anim.end, tracks: tracks},
type: 'object',
data_ref: '',
transform: null}]});
for(o in iron.Scene.active.raw.objects)
if (o.name == object.name) o.object_actions.push('action_'+actionR);
}
}
}
}
}
animation.play(reverse ? actionR : action, function() {
runOutput(1);
}, blendTime, speed, loop);
// Get the sampler for the played action
if (animation.activeActions != null) {
for (key in animation.activeActions.keys()) {
var s = animation.activeActions.get(key);
if (s.action == (reverse ? actionR : action)) {
sampler = s;
break;
}
}
}
if (sampler != null) {
sampler.setFrameOffset(startFrame);
}
runOutput(0);
}
}

View File

@ -2,7 +2,7 @@ package leenkx.logicnode;
import iron.object.MeshObject; import iron.object.MeshObject;
class PlayTilesheetNode extends LogicNode { class PlayTilesheetActionNode extends LogicNode {
public function new(tree: LogicTree) { public function new(tree: LogicTree) {
super(tree); super(tree);
@ -12,9 +12,9 @@ class PlayTilesheetNode extends LogicNode {
var object: MeshObject = inputs[1].get(); var object: MeshObject = inputs[1].get();
var action: String = inputs[2].get(); var action: String = inputs[2].get();
if (object == null) return; if (object == null || object.tilesheet == null) return;
object.activeTilesheet.play(action, function() { object.tilesheet.play(action, function() {
runOutput(1); runOutput(1);
}); });

View File

@ -11,7 +11,7 @@ class RemoveParticleFromObjectNode extends LogicNode {
} }
override function run(from: Int) { override function run(from: Int) {
#if lnx_particles #if lnx_gpu_particles
var object: Object = inputs[1].get(); var object: Object = inputs[1].get();
if (object == null) return; if (object == null) return;

View File

@ -0,0 +1,71 @@
package leenkx.logicnode;
import iron.object.Object;
import iron.math.Quat;
import iron.math.Vec4;
import leenkx.trait.physics.RigidBody;
class ReplaceObjectNode extends LogicNode {
public function new(tree: LogicTree) {
super(tree);
}
override function run(from: Int) {
var base: Object = inputs[1].get();
var replace: Object = inputs[2].get();
var invert: Bool = inputs[3].get();
var includeScale: Bool = inputs[4].get();
if (base == null || replace == null) return;
// Obtener posicion, rotacion y escala global de ambos obj // get position, rotation and global scale of both obj
var baseLoc = base.transform.world.getLoc().clone();
var replaceLoc = replace.transform.world.getLoc().clone();
var tmpVec = new Vec4();
var baseRot = new Quat();
var replaceRot = new Quat();
var tmpScale = new Vec4();
base.transform.world.decompose(tmpVec, baseRot, tmpScale);
replace.transform.world.decompose(tmpVec, replaceRot, tmpScale);
var baseScale = base.transform.scale.clone();
var replaceScale = replace.transform.scale.clone();
if (!invert) {
// Intercambiar transformaciones // Swap transformations
base.transform.loc.setFrom(replaceLoc);
base.transform.rot.setFrom(replaceRot);
if (includeScale) base.transform.scale.setFrom(replaceScale);
replace.transform.loc.setFrom(baseLoc);
replace.transform.rot.setFrom(baseRot);
if (includeScale) replace.transform.scale.setFrom(baseScale);
} else {
// Invertir: cada (object) vuelve a su lugar inicial // Invert: each (object) returns to its initial place
base.transform.loc.setFrom(baseLoc);
base.transform.rot.setFrom(baseRot);
if (includeScale) base.transform.scale.setFrom(baseScale);
replace.transform.loc.setFrom(replaceLoc);
replace.transform.rot.setFrom(replaceRot);
if (includeScale) replace.transform.scale.setFrom(replaceScale);
}
// Recalcular matrices
base.transform.buildMatrix();
replace.transform.buildMatrix();
// Sincronizar RB // Sync RB
#if lnx_physics
for (obj in [base, replace]) {
var rb = obj.getTrait(RigidBody);
if (rb != null) rb.syncTransform();
}
#end
runOutput(0);
}
}

View File

@ -11,9 +11,9 @@ class ResumeTilesheetNode extends LogicNode {
override function run(from: Int) { override function run(from: Int) {
var object: MeshObject = inputs[1].get(); var object: MeshObject = inputs[1].get();
if (object == null) return; if (object == null || object.tilesheet == null) return;
object.activeTilesheet.resume(); object.tilesheet.resume();
runOutput(0); runOutput(0);
} }

View File

@ -1,5 +1,9 @@
package leenkx.logicnode; package leenkx.logicnode;
import iron.math.Quat;
import iron.math.Vec4;
import iron.math.Mat4;
class RetainValueNode extends LogicNode { class RetainValueNode extends LogicNode {
var value: Dynamic = null; var value: Dynamic = null;
@ -9,7 +13,19 @@ class RetainValueNode extends LogicNode {
} }
override function run(from: Int) { override function run(from: Int) {
value = inputs[1].get(); var retainValue = inputs[1].get();
switch(Type.getClassName(Type.getClass(retainValue))){
case "iron.math.Vec4":
value = (cast retainValue: Vec4).clone();
case "iron.math.Mat4":
value = (cast retainValue: Mat4).clone();
case "iron.math.Quat":
var q: Quat = new Quat();
value = q.setFrom((cast retainValue: Quat));
default:
value = retainValue;
}
runOutput(0); runOutput(0);
} }

View File

@ -0,0 +1,67 @@
package leenkx.logicnode;
import iron.object.Object;
class SetFirstPersonControllerNode extends LogicNode {
public function new(tree: LogicTree) {
super(tree);
}
override function run(from: Int): Void {
// Control de las var de FirstPersonController...
// Control FirstPersonController var
var object: Object = inputs[1].get();
// Ajustes de la camara. // Camera settings
var rotationSpeed: Float = inputs[2].get();
var maxPitch: Float = inputs[3].get();
var minPitch: Float = inputs[4].get();
// Ajustes de desplazamiento.. // Move settings
var moveSpeed: Float = inputs[5].get();
var runSpeed: Float = inputs[6].get();
// var bool to set true/false the trait props (FirstPersonController)
var enableJump: Bool = inputs[7].get();
var allowAirJump: Bool = inputs[8].get();
var canRun: Bool = inputs[9].get();
var stamina: Bool = inputs[10].get();
var enableFatigue: Bool = inputs[11].get();
if (object == null) return;
// Tomar el Trait desde el object (FPController) // Get trait from (object) and assigned to the var (fpController)
var fpController: leenkx.trait.FirstPersonController = object.getTrait(leenkx.trait.FirstPersonController);
if (fpController != null) {
// Ajustes de la camara // Cam settings
fpController.rotationSpeed = rotationSpeed;
fpController.maxPitch = maxPitch;
fpController.minPitch = minPitch;
// Ajuste de desplazamiento // Move settings
fpController.moveSpeed = moveSpeed;
fpController.runSpeed = runSpeed;
// Settings (run, jump, stamina anad fatigue)
fpController.enableJump = enableJump;
fpController.allowAirJump = allowAirJump;
fpController.canRun = canRun;
fpController.stamina = stamina;
fpController.enableFatigue = enableFatigue;
} else {
// Alert the user if they do not have the trait assigning to the object.
trace("ERROR: The object '" + object.name + "' does not have the FirstPersonController script assigning(assign it from (Object->add trait->bundle)).");
}
runOutput(0);
}
}

View File

@ -0,0 +1,65 @@
package leenkx.logicnode;
import iron.object.Object;
class SetOverheadPersonControllerNode extends LogicNode {
public function new(tree: LogicTree) {
super(tree);
}
override function run(from: Int): Void {
// Control de las var de OverheadPersonController...
// Control OverheadPersonController var
var objectTrait: Object = inputs[1].get();
// Ajustes de la camara. // Camera settings
var smoothTrack: Bool = inputs[2].get();
var smoothSpeed: Float = inputs[3].get();
// Ajustes de desplazamiento.. // Move setting
var moveSpeed: Float = inputs[4].get();
var runSpeed: Float = inputs[5].get();
// var bool to set true/false the trait props (OverheadPersonController)
var enableJump: Bool = inputs[6].get();
var allowAirJump: Bool = inputs[7].get();
var canRun: Bool = inputs[8].get();
var stamina: Bool = inputs[9].get();
var enableFatigue: Bool = inputs[10].get();
if (objectTrait == null) return;
// Tomar el trait del object (ovController). // Get trait from (objectTrait) and assigned to the var (ovController)
var ovController: leenkx.trait.OverheadPersonController = objectTrait.getTrait(leenkx.trait.OverheadPersonController);
if (ovController != null) {
// Setting smoothCamera
ovController.smoothTrack = smoothTrack;
ovController.smoothSpeed = smoothSpeed;
// Setting move
ovController.moveSpeed = moveSpeed;
ovController.runSpeed = runSpeed;
// Settings (run, jump, stamina anad fatigue)
ovController.enableJump = enableJump;
ovController.allowAirJump = allowAirJump;
ovController.canRun = canRun;
ovController.stamina = stamina;
ovController.enableFatigue = enableFatigue;
} else {
// Alert the user if they do not have the trait assigning to the object..
trace("ERROR: The object '" + objectTrait.name + "' does not have the OverheadPersonController script assigning(assign it from (Object->add trait->bundle)).");
}
runOutput(0);
}
}

View File

@ -11,7 +11,7 @@ class SetParticleDataNode extends LogicNode {
} }
override function run(from: Int) { override function run(from: Int) {
#if lnx_particles #if lnx_gpu_particles
var object: Object = inputs[1].get(); var object: Object = inputs[1].get();
var slot: Int = inputs[2].get(); var slot: Int = inputs[2].get();
@ -41,7 +41,7 @@ class SetParticleDataNode extends LogicNode {
var emit_from: Int = inputs[3].get(); var emit_from: Int = inputs[3].get();
if (emit_from == 0 || emit_from == 1 || emit_from == 2) { if (emit_from == 0 || emit_from == 1 || emit_from == 2) {
@:privateAccess psys.r.emit_from = emit_from; @:privateAccess psys.r.emit_from = emit_from;
@:privateAccess psys.setupGeomGpu(mo.particleChildren != null ? mo.particleChildren[slot] : cast(iron.Scene.active.getChild(@:privateAccess psys.data.raw.instance_object), iron.object.MeshObject), mo); @:privateAccess psys.setupGeomGpu(mo.particleChildren != null ? mo.particleChildren[slot] : cast(iron.Scene.active.getChild(@:privateAccess psys.data.raw.instance_object), iron.object.MeshObject));
} }
case 'Auto Start': case 'Auto Start':
@:privateAccess psys.r.auto_start = inputs[3].get(); @:privateAccess psys.r.auto_start = inputs[3].get();

View File

@ -9,7 +9,7 @@ class SetParticleRenderEmitterNode extends LogicNode {
} }
override function run(from: Int) { override function run(from: Int) {
#if lnx_particles #if lnx_gpu_particles
var object: Object = inputs[1].get(); var object: Object = inputs[1].get();
if (object == null) return; if (object == null) return;

View File

@ -9,7 +9,7 @@ class SetParticleSpeedNode extends LogicNode {
} }
override function run(from: Int) { override function run(from: Int) {
#if lnx_particles #if lnx_gpu_particles
var object: Object = inputs[1].get(); var object: Object = inputs[1].get();
var slot: Int = inputs[2].get(); var slot: Int = inputs[2].get();
var speed: Float = inputs[3].get(); var speed: Float = inputs[3].get();

View File

@ -0,0 +1,21 @@
package leenkx.logicnode;
import iron.object.MeshObject;
class SetTilesheetActionNode extends LogicNode {
public function new(tree: LogicTree) {
super(tree);
}
override function run(from: Int) {
var object: MeshObject = inputs[1].get();
var action: String = inputs[2].get();
if (object == null || object.tilesheet == null) return;
object.setTilesheetAction(action);
runOutput(0);
}
}

View File

@ -1,9 +1,8 @@
package leenkx.logicnode; package leenkx.logicnode;
import iron.Scene;
import iron.object.MeshObject; import iron.object.MeshObject;
class SetActiveTilesheetNode extends LogicNode { class SetTilesheetFlipNode extends LogicNode {
public function new(tree: LogicTree) { public function new(tree: LogicTree) {
super(tree); super(tree);
@ -11,12 +10,13 @@ class SetActiveTilesheetNode extends LogicNode {
override function run(from: Int) { override function run(from: Int) {
var object: MeshObject = inputs[1].get(); var object: MeshObject = inputs[1].get();
var tilesheet: String = inputs[2].get(); var flipX: Bool = inputs[2].get();
var action: String = inputs[3].get(); var flipY: Bool = inputs[3].get();
if (object == null) return; if (object == null) return;
if (object.tilesheet == null) return;
object.setActiveTilesheet(Scene.active.raw.name, tilesheet, action); object.tilesheet.flipX = flipX;
object.tilesheet.flipY = flipY;
runOutput(0); runOutput(0);
} }

View File

@ -12,9 +12,9 @@ class SetTilesheetFrameNode extends LogicNode {
var object: MeshObject = inputs[1].get(); var object: MeshObject = inputs[1].get();
var frame: Int = inputs[2].get(); var frame: Int = inputs[2].get();
if (object == null) return; if (object == null || object.tilesheet == null) return;
object.activeTilesheet.setFrameOffset(frame); object.tilesheet.setFrameOffset(frame);
runOutput(0); runOutput(0);
} }

View File

@ -12,9 +12,9 @@ class SetTilesheetPausedNode extends LogicNode {
var object: MeshObject = inputs[1].get(); var object: MeshObject = inputs[1].get();
var paused: Bool = inputs[2].get(); var paused: Bool = inputs[2].get();
if (object == null) return; if (object == null || object.tilesheet == null) return;
paused ? object.activeTilesheet.pause() : object.activeTilesheet.resume(); paused ? object.tilesheet.pause() : object.tilesheet.resume();
runOutput(0); runOutput(0);
} }

View File

@ -8,5 +8,6 @@ class ShutdownNode extends LogicNode {
override function run(from: Int) { override function run(from: Int) {
kha.System.stop(); kha.System.stop();
runOutput(0);
} }
} }

View File

@ -0,0 +1,26 @@
package leenkx.logicnode;
import leenkx.system.Signal;
import iron.object.Object;
class SignalNode extends LogicNode {
var signal: Signal = null;
public function new(tree: LogicTree) {
super(tree);
}
override function get(from: Int): Null<Signal> {
var object: Object = inputs[0].get();
var property: String = inputs[1].get();
if (object != null && property != null && property != "") {
return Reflect.getProperty(object, property);
}
if (signal == null) {
signal = new Signal();
}
return signal;
}
}

View File

@ -1,6 +1,7 @@
package leenkx.logicnode; package leenkx.logicnode;
import iron.object.CameraObject; import iron.object.CameraObject;
import kha.Color;
class WriteImageNode extends LogicNode { class WriteImageNode extends LogicNode {
@ -43,6 +44,34 @@ class WriteImageNode extends LogicNode {
camera.renderTarget = oldRT; camera.renderTarget = oldRT;
iron.Scene.active.camera = sceneCam; iron.Scene.active.camera = sceneCam;
if (inputs[9].get()){
tex = kha.Image.createRenderTarget(inputs[3].get(), inputs[4].get(),
kha.graphics4.TextureFormat.RGBA32,
kha.graphics4.DepthStencilFormat.NoDepthAndStencil);
tex.g2.begin(true, Color.Transparent);
tex.g2.color = Color.White;
tex.g2.drawScaledImage(renderTarget, 0, 0, inputs[3].get(), inputs[4].get());
var scl = inputs[3].get() / iron.App.w();
if (kha.Image.renderTargetsInvertedY()){
tex.g2.scale(scl, -scl);
tex.g2.translate(0, inputs[4].get());
}
else
tex.g2.scale(scl, scl);
for (f in @:privateAccess iron.App.traitRenders2D){
f(tex.g2);
}
tex.g2.end();
}
var pixels = tex.getPixels(); var pixels = tex.getPixels();
for (i in 0...pixels.length){ for (i in 0...pixels.length){

View File

@ -1,6 +1,6 @@
package leenkx.network; package leenkx.network;
@:enum abstract OpCode(Int) { enum abstract OpCode(Int) {
var Continuation = 0x00; var Continuation = 0x00;
var Text = 0x01; var Text = 0x01;
var Binary = 0x02; var Binary = 0x02;

View File

@ -6,7 +6,7 @@ import haxe.io.Bytes;
typedef BinaryType = js.html.BinaryType; typedef BinaryType = js.html.BinaryType;
#else #else
@:enum abstract BinaryType(String) { enum abstract BinaryType(String) {
var ARRAYBUFFER = "arraybuffer"; var ARRAYBUFFER = "arraybuffer";
@:to public function toString() { @:to public function toString() {

View File

@ -10,8 +10,6 @@ class Inc {
static var path: RenderPath; static var path: RenderPath;
public static var superSample = 1.0; public static var superSample = 1.0;
static var pointIndex = 0;
static var spotIndex = 0;
static var lastFrame = -1; static var lastFrame = -1;
#if lnx_shadowmap_atlas #if lnx_shadowmap_atlas
@ -41,6 +39,7 @@ class Inc {
#if (rp_voxels == "Voxel GI") #if (rp_voxels == "Voxel GI")
static var voxel_td1:kha.compute.TextureUnit; static var voxel_td1:kha.compute.TextureUnit;
static var voxel_te1:kha.compute.TextureUnit; static var voxel_te1:kha.compute.TextureUnit;
static var voxel_tf1:kha.compute.TextureUnit;
static var voxel_cc1:kha.compute.ConstantLocation; static var voxel_cc1:kha.compute.ConstantLocation;
#else #else
#if lnx_voxelgi_shadows #if lnx_voxelgi_shadows
@ -94,8 +93,34 @@ class Inc {
static var voxel_cb4:kha.compute.ConstantLocation; static var voxel_cb4:kha.compute.ConstantLocation;
static var voxel_cc4:kha.compute.ConstantLocation; static var voxel_cc4:kha.compute.ConstantLocation;
static var voxel_cd4:kha.compute.ConstantLocation; static var voxel_cd4:kha.compute.ConstantLocation;
static var voxel_sh5:kha.compute.Shader = null;
static var voxel_ta5:kha.compute.TextureUnit;
static var voxel_te5:kha.compute.TextureUnit;
static var voxel_tf5:kha.compute.TextureUnit;
static var voxel_tg5:kha.compute.TextureUnit;
static var voxel_ca5:kha.compute.ConstantLocation;
static var voxel_cb5:kha.compute.ConstantLocation;
static var voxel_cc5:kha.compute.ConstantLocation;
static var voxel_cd5:kha.compute.ConstantLocation;
static var voxel_ce5:kha.compute.ConstantLocation;
static var voxel_cf5:kha.compute.ConstantLocation;
static var voxel_cg5:kha.compute.ConstantLocation;
#if rp_shadowmap
static var voxel_tb5:kha.compute.TextureUnit;
static var voxel_tc5:kha.compute.TextureUnit;
static var voxel_td5:kha.compute.TextureUnit;
static var voxel_ch5:kha.compute.ConstantLocation;
static var voxel_ci5:kha.compute.ConstantLocation;
static var voxel_cj5:kha.compute.ConstantLocation;
static var voxel_ck5:kha.compute.ConstantLocation;
#if lnx_shadowmap_atlas
static var voxel_cl5:kha.compute.ConstantLocation;
static var voxel_cm5:kha.compute.ConstantLocation;
#end #end
#end //rp_voxels #end
#end //rp_voxels == "Voxel GI"
#end //rp_voxels != "Off"
public static function init(_path: RenderPath) { public static function init(_path: RenderPath) {
path = _path; path = _path;
@ -440,33 +465,39 @@ class Inc {
path.bindTarget(n, n); path.bindTarget(n, n);
break; break;
} }
for (i in 0...pointIndex) { var lightIndex = 0;
var n = "shadowMapPoint[" + i + "]"; for (l in iron.Scene.active.lights) {
if (iron.object.LightObject.discardLightCulled(l)) continue;
if (l.data.raw.type == "point") {
var n = "shadowMapPoint[" + lightIndex + "]";
path.bindTarget(n, n); path.bindTarget(n, n);
var n = "shadowMapPointTransparent[" + i + "]"; var n = "shadowMapPointTransparent[" + lightIndex + "]";
path.bindTarget(n, n); path.bindTarget(n, n);
} }
for (i in 0...spotIndex) { else if (l.data.raw.type == "spot" || l.data.raw.type == "area") {
var n = "shadowMapSpot[" + i + "]"; var n = "shadowMapSpot[" + lightIndex + "]";
path.bindTarget(n, n); path.bindTarget(n, n);
var n = "shadowMapSpotTransparent[" + i + "]"; var n = "shadowMapSpotTransparent[" + lightIndex + "]";
path.bindTarget(n, n); path.bindTarget(n, n);
} }
lightIndex++;
}
} }
static function shadowMapName(light: LightObject, transparent: Bool): String { static function shadowMapName(light: LightObject, index: Int, transparent: Bool): String {
switch (light.data.raw.type) { switch (light.data.raw.type) {
case "sun": case "sun":
return transparent ? "shadowMapTransparent" : "shadowMap"; return transparent ? "shadowMapTransparent" : "shadowMap";
case "point": case "point":
return transparent ? "shadowMapPointTransparent[" + pointIndex + "]" : "shadowMapPoint[" + pointIndex + "]"; return transparent ? "shadowMapPointTransparent[" + index + "]" : "shadowMapPoint[" + index + "]";
default: default:
return transparent ? "shadowMapSpotTransparent[" + spotIndex + "]" : "shadowMapSpot[" + spotIndex + "]"; return transparent ? "shadowMapSpotTransparent[" + index + "]" : "shadowMapSpot[" + index + "]";
} }
} }
static function getShadowMap(l: iron.object.LightObject, transparent: Bool): String { static function getShadowMap(l: iron.object.LightObject, index: Int, transparent: Bool): String {
var target = shadowMapName(l, transparent); var target = shadowMapName(l, index, transparent);
var rt = path.renderTargets.get(target); var rt = path.renderTargets.get(target);
// Create shadowmap on the fly // Create shadowmap on the fly
if (rt == null) { if (rt == null) {
@ -509,13 +540,12 @@ class Inc {
lastFrame = RenderPath.active.frame; lastFrame = RenderPath.active.frame;
#end #end
pointIndex = 0; var lightIndex = 0;
spotIndex = 0;
for (l in iron.Scene.active.lights) { for (l in iron.Scene.active.lights) {
if (!l.visible) continue; if (!l.visible) continue;
path.light = l; path.light = l;
var shadowmap = Inc.getShadowMap(l, false); var shadowmap = Inc.getShadowMap(l, lightIndex, false);
var faces = l.data.raw.shadowmap_cube ? 6 : 1; var faces = l.data.raw.shadowmap_cube ? 6 : 1;
for (i in 0...faces) { for (i in 0...faces) {
if (faces > 1) path.currentFace = i; if (faces > 1) path.currentFace = i;
@ -527,18 +557,18 @@ class Inc {
} }
path.currentFace = -1; path.currentFace = -1;
if (l.data.raw.type == "point") pointIndex++; if (!iron.object.LightObject.discardLightCulled(l)) {
else if (l.data.raw.type == "spot" || l.data.raw.type == "area") spotIndex++; lightIndex++;
}
} }
#if rp_shadowmap_transparent #if rp_shadowmap_transparent
pointIndex = 0; lightIndex = 0;
spotIndex = 0;
for (l in iron.Scene.active.lights) { for (l in iron.Scene.active.lights) {
if (!l.visible) continue; if (!l.visible) continue;
path.light = l; path.light = l;
var shadowmap_transparent = Inc.getShadowMap(l, true); var shadowmap_transparent = Inc.getShadowMap(l, lightIndex, true);
var faces = l.data.raw.shadowmap_cube ? 6 : 1; var faces = l.data.raw.shadowmap_cube ? 6 : 1;
for (i in 0...faces) { for (i in 0...faces) {
if (faces > 1) path.currentFace = i; if (faces > 1) path.currentFace = i;
@ -550,8 +580,9 @@ class Inc {
} }
path.currentFace = -1; path.currentFace = -1;
if (l.data.raw.type == "point") pointIndex++; if (!iron.object.LightObject.discardLightCulled(l)) {
else if (l.data.raw.type == "spot" || l.data.raw.type == "area") spotIndex++; lightIndex++;
}
} }
#end #end
#end // rp_shadowmap #end // rp_shadowmap
@ -606,6 +637,9 @@ class Inc {
// Init voxels // Init voxels
#if (rp_voxels != 'Off') #if (rp_voxels != 'Off')
if (!voxelsCreated) initGI(); if (!voxelsCreated) initGI();
#if (rp_voxels == "Voxel GI")
initGI("voxelsLight");
#end
#end #end
#end // lnx_config #end // lnx_config
} }
@ -732,6 +766,12 @@ class Inc {
t.height = res * Main.voxelgiClipmapCount; t.height = res * Main.voxelgiClipmapCount;
t.depth = res; t.depth = res;
} }
else if (t.name == "voxelsLight") {
t.format = "RGBA32";
t.width = res;
t.height = res * Main.voxelgiClipmapCount;
t.depth = res;
}
else { else {
#if (rp_voxels == "Voxel AO") #if (rp_voxels == "Voxel AO")
{ {
@ -876,7 +916,8 @@ class Inc {
#if (rp_voxels == "Voxel GI") #if (rp_voxels == "Voxel GI")
voxel_td1 = voxel_sh1.getTextureUnit("voxelsSampler"); voxel_td1 = voxel_sh1.getTextureUnit("voxelsSampler");
voxel_te1 = voxel_sh1.getTextureUnit("SDF"); voxel_te1 = voxel_sh1.getTextureUnit("voxelsLight");
voxel_tf1 = voxel_sh1.getTextureUnit("SDF");
voxel_cc1 = voxel_sh1.getConstantLocation("envmapStrength"); voxel_cc1 = voxel_sh1.getConstantLocation("envmapStrength");
#else #else
#if lnx_voxelgi_shadows #if lnx_voxelgi_shadows
@ -949,6 +990,37 @@ class Inc {
voxel_cc4 = voxel_sh4.getConstantLocation("eye"); voxel_cc4 = voxel_sh4.getConstantLocation("eye");
voxel_cd4 = voxel_sh4.getConstantLocation("postprocess_resolution"); voxel_cd4 = voxel_sh4.getConstantLocation("postprocess_resolution");
} }
if (voxel_sh5 == null)
{
voxel_sh5 = path.getComputeShader("voxel_light");
voxel_ta5 = voxel_sh5.getTextureUnit("voxelsLight");
voxel_te5 = voxel_sh5.getTextureUnit("voxels");
voxel_tf5 = voxel_sh5.getTextureUnit("voxelsSampler");
voxel_tg5 = voxel_sh5.getTextureUnit("voxelsSDFSampler");
voxel_ca5 = voxel_sh5.getConstantLocation("clipmaps");
voxel_cb5 = voxel_sh5.getConstantLocation("clipmapLevel");
voxel_cc5 = voxel_sh5.getConstantLocation("lightPos");
voxel_cd5 = voxel_sh5.getConstantLocation("lightColor");
voxel_ce5 = voxel_sh5.getConstantLocation("lightType");
voxel_cf5 = voxel_sh5.getConstantLocation("lightDir");
voxel_cg5 = voxel_sh5.getConstantLocation("spotData");
#if rp_shadowmap
voxel_tb5 = voxel_sh5.getTextureUnit("shadowMap");
voxel_tc5 = voxel_sh5.getTextureUnit("shadowMapSpot");
voxel_td5 = voxel_sh5.getTextureUnit("shadowMapPoint");
voxel_ch5 = voxel_sh5.getConstantLocation("lightShadow");
voxel_ci5 = voxel_sh5.getConstantLocation("lightProj");
voxel_cj5 = voxel_sh5.getConstantLocation("LVP");
voxel_ck5 = voxel_sh5.getConstantLocation("shadowsBias");
#if lnx_shadowmap_atlas
voxel_cl5 = voxel_sh5.getConstantLocation("index");
voxel_cm5 = voxel_sh5.getConstantLocation("pointLightDataArray");
#end
#end
}
#end #end
} }
@ -998,7 +1070,8 @@ class Inc {
kha.compute.Compute.setTexture(voxel_tc1, rts.get("voxelsOut").image, kha.compute.Access.Write); kha.compute.Compute.setTexture(voxel_tc1, rts.get("voxelsOut").image, kha.compute.Access.Write);
#if (rp_voxels == "Voxel GI") #if (rp_voxels == "Voxel GI")
kha.compute.Compute.setSampledTexture(voxel_td1, rts.get("voxelsOutB").image); kha.compute.Compute.setSampledTexture(voxel_td1, rts.get("voxelsOutB").image);
kha.compute.Compute.setTexture(voxel_te1, rts.get("voxelsSDF").image, kha.compute.Access.Write); kha.compute.Compute.setTexture(voxel_te1, rts.get("voxelsLight").image, kha.compute.Access.Read);
kha.compute.Compute.setTexture(voxel_tf1, rts.get("voxelsSDF").image, kha.compute.Access.Write);
kha.compute.Compute.setFloat(voxel_cc1, iron.Scene.active.world == null ? 0.0 : iron.Scene.active.world.probe.raw.strength); kha.compute.Compute.setFloat(voxel_cc1, iron.Scene.active.world == null ? 0.0 : iron.Scene.active.world.probe.raw.strength);
#else #else
#if lnx_voxelgi_shadows #if lnx_voxelgi_shadows
@ -1270,6 +1343,7 @@ class Inc {
kha.compute.Compute.compute(Std.int((width + 7) / 8), Std.int((height + 7) / 8), 1); kha.compute.Compute.compute(Std.int((width + 7) / 8), Std.int((height + 7) / 8), 1);
} }
#end
public static function resolveSpecular() { public static function resolveSpecular() {
var rts = path.renderTargets; var rts = path.renderTargets;
@ -1334,6 +1408,149 @@ class Inc {
kha.compute.Compute.compute(Std.int((width + 7) / 8), Std.int((height + 7) / 8), 1); kha.compute.Compute.compute(Std.int((width + 7) / 8), Std.int((height + 7) / 8), 1);
} }
#if (rp_voxels == "Voxel GI")
public static function computeVoxelsLight() {
var rts = path.renderTargets;
var res = iron.RenderPath.getVoxelRes();
var camera = iron.Scene.active.camera;
var clipmaps = iron.RenderPath.clipmaps;
var clipmap = clipmaps[iron.RenderPath.clipmapLevel];
var lights = iron.Scene.active.lights;
var lightIndex = 0;
for (i in 0...lights.length) {
var l = lights[i];
if (!l.visible) continue;
path.light = l;
kha.compute.Compute.setShader(voxel_sh5);
kha.compute.Compute.setTexture(voxel_ta5, rts.get("voxelsLight").image, kha.compute.Access.Write);
kha.compute.Compute.setTexture(voxel_te5, rts.get("voxels").image, kha.compute.Access.Read);
kha.compute.Compute.setSampledTexture(voxel_tf5, rts.get("voxelsOut").image);
kha.compute.Compute.setSampledTexture(voxel_tg5, rts.get("voxelsSDF").image);
var fa:Float32Array = new Float32Array(Main.voxelgiClipmapCount * 10);
for (i in 0...Main.voxelgiClipmapCount) {
fa[i * 10] = clipmaps[i].voxelSize;
fa[i * 10 + 1] = clipmaps[i].extents.x;
fa[i * 10 + 2] = clipmaps[i].extents.y;
fa[i * 10 + 3] = clipmaps[i].extents.z;
fa[i * 10 + 4] = clipmaps[i].center.x;
fa[i * 10 + 5] = clipmaps[i].center.y;
fa[i * 10 + 6] = clipmaps[i].center.z;
fa[i * 10 + 7] = clipmaps[i].offset_prev.x;
fa[i * 10 + 8] = clipmaps[i].offset_prev.y;
fa[i * 10 + 9] = clipmaps[i].offset_prev.z;
}
kha.compute.Compute.setFloats(voxel_ca5, fa);
kha.compute.Compute.setInt(voxel_cb5, iron.RenderPath.clipmapLevel);
#if rp_shadowmap
if (l.data.raw.type == "sun") {
#if lnx_shadowmap_atlas
#if lnx_shadowmap_atlas_single_map
kha.compute.Compute.setSampledTexture(voxel_tb5, rts.get("shadowMapAtlas").image);
#else
kha.compute.Compute.setSampledTexture(voxel_tb5, rts.get("shadowMapAtlasSun").image);
#end
#else
kha.compute.Compute.setSampledTexture(voxel_tb5, rts.get("shadowMap").image);
#end
kha.compute.Compute.setInt(voxel_ch5, 1); // lightShadow
}
else if (l.data.raw.type == "spot" || l.data.raw.type == "area") {
#if lnx_shadowmap_atlas
#if lnx_shadowmap_atlas_single_map
kha.compute.Compute.setSampledTexture(voxel_tc5, rts.get("shadowMapAtlas").image);
#else
kha.compute.Compute.setSampledTexture(voxel_tc5, rts.get("shadowMapAtlasSpot").image);
#end
#else
kha.compute.Compute.setSampledTexture(voxel_tc5, rts.get("shadowMapSpot[" + lightIndex + "]").image);
#end
kha.compute.Compute.setInt(voxel_ch5, 2);
}
else {
#if lnx_shadowmap_atlas
#if lnx_shadowmap_atlas_single_map
kha.compute.Compute.setSampledTexture(voxel_td5, rts.get("shadowMapAtlas").image);
#else
kha.compute.Compute.setSampledTexture(voxel_td5, rts.get("shadowMapAtlasPoint").image);
kha.compute.Compute.setInt(voxel_cl5, i);
kha.compute.Compute.setFloats(voxel_cm5, iron.object.LightObject.pointLightsData);
#end
#else
kha.compute.Compute.setSampledCubeMap(voxel_td5, rts.get("shadowMapPoint[" + lightIndex + "]").cubeMap);
#end
kha.compute.Compute.setInt(voxel_ch5, 3);
}
// lightProj
var near = l.data.raw.near_plane;
var far = l.data.raw.far_plane;
var a:kha.FastFloat = far + near;
var b:kha.FastFloat = far - near;
var f2:kha.FastFloat = 2.0;
var c:kha.FastFloat = f2 * far * near;
var vx:kha.FastFloat = a / b;
var vy:kha.FastFloat = c / b;
kha.compute.Compute.setFloat2(voxel_ci5, vx, vy);
// LVP
m.setFrom(l.VP);
m.multmat(iron.object.Uniforms.biasMat);
#if lnx_shadowmap_atlas
if (l.data.raw.type == "sun")
{
// tile matrix
m.setIdentity();
// scale [0-1] coords to [0-tilescale]
m._00 = l.tileScale[0];
m._11 = l.tileScale[0];
// offset coordinate start from [0, 0] to [tile-start-x, tile-start-y]
m._30 = l.tileOffsetX[0];
m._31 = l.tileOffsetY[0];
m.multmat(m);
#if (!kha_opengl)
m.setIdentity();
m._11 = -1.0;
m._31 = 1.0;
m.multmat(m);
#end
}
#end
kha.compute.Compute.setMatrix(voxel_cj5, m.self);
// shadowsBias
kha.compute.Compute.setFloat(voxel_ck5, l.data.raw.shadows_bias);
#end // rp_shadowmap
// lightPos
kha.compute.Compute.setFloat3(voxel_cc5, l.transform.worldx(), l.transform.worldy(), l.transform.worldz());
// lightCol
var f = l.data.raw.strength;
kha.compute.Compute.setFloat3(voxel_cd5, l.data.raw.color[0] * f, l.data.raw.color[1] * f, l.data.raw.color[2] * f);
// lightType
kha.compute.Compute.setInt(voxel_ce5, iron.data.LightData.typeToInt(l.data.raw.type));
// lightDir
var v = l.look();
kha.compute.Compute.setFloat3(voxel_cf5, v.x, v.y, v.z);
// spotData
if (l.data.raw.type == "spot") {
var vx = l.data.raw.spot_size;
var vy = vx - l.data.raw.spot_blend;
kha.compute.Compute.setFloat2(voxel_cg5, vx, vy);
}
kha.compute.Compute.compute(Std.int(res / 8), Std.int(res / 8), Std.int(res / 8));
if (!iron.object.LightObject.discardLightCulled(l)) {
lightIndex++;
}
}
}
#end // GI #end // GI
#end // Voxels #end // Voxels
} }

View File

@ -342,12 +342,6 @@ class Postprocess {
v.x = ssao_uniforms[0]; //SSAO Strength v.x = ssao_uniforms[0]; //SSAO Strength
v.y = ssao_uniforms[1]; //SSAO Radius v.y = ssao_uniforms[1]; //SSAO Radius
v.z = ssao_uniforms[2]; //SSAO Max Steps v.z = ssao_uniforms[2]; //SSAO Max Steps
case "_PPComp13":
v = iron.object.Uniforms.helpVec;
v.x = chromatic_aberration_uniforms[0]; //CA Strength
v.y = chromatic_aberration_uniforms[1]; //CA Samples
v.z = chromatic_aberration_uniforms[2]; //CA Type
v.w = chromatic_aberration_uniforms[3]; //On/Off
case "_PPComp14": case "_PPComp14":
v = iron.object.Uniforms.helpVec; v = iron.object.Uniforms.helpVec;
v.x = camera_uniforms[10]; //Distort v.x = camera_uniforms[10]; //Distort
@ -377,6 +371,12 @@ class Postprocess {
v.y = letterbox_uniforms[0][1]; v.y = letterbox_uniforms[0][1];
v.z = letterbox_uniforms[0][2]; v.z = letterbox_uniforms[0][2];
v.w = letterbox_uniforms[1][0]; //Size v.w = letterbox_uniforms[1][0]; //Size
case "_PPComp13":
v = iron.object.Uniforms.helpVec;
v.x = chromatic_aberration_uniforms[0]; //CA Strength
v.y = chromatic_aberration_uniforms[1]; //CA Samples
v.z = chromatic_aberration_uniforms[2]; //CA Type
v.w = chromatic_aberration_uniforms[3]; //On/Off
case "_PPComp16": case "_PPComp16":
v = iron.object.Uniforms.helpVec; v = iron.object.Uniforms.helpVec;
v.x = sharpen_uniforms[0][0]; //Color v.x = sharpen_uniforms[0][0]; //Color

View File

@ -712,6 +712,7 @@ class RenderPathDeferred {
Inc.computeVoxelsTemporal(); Inc.computeVoxelsTemporal();
#if (rp_voxels == "Voxel GI") #if (rp_voxels == "Voxel GI")
Inc.computeVoxelsLight();
Inc.computeVoxelsSDF(); Inc.computeVoxelsSDF();
#end #end

View File

@ -448,6 +448,10 @@ class RenderPathForward {
Inc.computeVoxelsTemporal(); Inc.computeVoxelsTemporal();
#if (rp_voxels == "Voxel GI")
Inc.computeVoxelsLight();
#end
#if (lnx_voxelgi_shadows || (rp_voxels == "Voxel GI")) #if (lnx_voxelgi_shadows || (rp_voxels == "Voxel GI"))
Inc.computeVoxelsSDF(); Inc.computeVoxelsSDF();
#end #end

View File

@ -60,7 +60,7 @@ class Starter {
#if lnx_patch #if lnx_patch
iron.Scene.getRenderPath = getRenderPath; iron.Scene.getRenderPath = getRenderPath;
#end #end
#if lnx_draworder_shader #if lnx_draworder_index
iron.RenderPath.active.drawOrder = iron.RenderPath.DrawOrder.Index; iron.RenderPath.active.drawOrder = iron.RenderPath.DrawOrder.Index;
#end // else Distance #end // else Distance
}); });

View File

@ -0,0 +1,60 @@
package leenkx.system;
import iron.App;
import iron.system.Time;
class Timer {
public var timeout: Signal = new Signal();
public var oneShot: Bool = true;
public var paused: Bool = true;
public var timeLeft: Float = 1.0;
public var waitTime: Float = 1.0;
public function new(time: Float = 1.0, oneShot: Bool = true) {
this.oneShot = oneShot;
this.waitTime = time;
this.timeLeft = time;
}
public function start(time: Float = -1.0) {
if (isStopped()) App.notifyOnUpdate(update);
if (time > 0) {
waitTime = time;
}
timeLeft = waitTime;
paused = false;
}
public function pause() {
paused = true;
}
public function stop() {
if (!isStopped()) App.removeUpdate(update);
paused = true;
timeLeft = waitTime;
}
public function isStopped(): Bool {
return paused && timeLeft == waitTime;
}
function update() {
if (paused) return;
timeLeft -= Time.delta;
if (timeLeft <= 0) {
timeout.emit();
if (oneShot) {
paused = true;
timeLeft = waitTime;
App.removeUpdate(update);
} else {
timeLeft += waitTime;
}
}
}
}

View File

@ -86,11 +86,14 @@ class FirstPersonController extends Trait {
var zVec = Vec4.zAxis(); var zVec = Vec4.zAxis();
// Control de la rotacion del jugador y la camara con el mouse..
// Control player and camera rotation with the mouse.
function preUpdate() { function preUpdate() {
if (Input.occupied || body == null) return; if (Input.occupied || body == null) return;
var mouse = Input.getMouse(); var mouse = Input.getMouse();
var kb = Input.getKeyboard(); var kb = Input.getKeyboard();
// Blooquear/Desbloquear cursor // lock/unlock cursor
if (mouse.started() && !mouse.locked) if (mouse.started() && !mouse.locked)
mouse.lock(); mouse.lock();
else if (kb.started("escape") && mouse.locked) else if (kb.started("escape") && mouse.locked)
@ -113,6 +116,7 @@ class FirstPersonController extends Trait {
return enableFatigue && isFatigueActive; return enableFatigue && isFatigueActive;
} }
// Comprobar si el jugador esta moviendose y si esta en el suelo // Check if the player is moving and if he is on the ground...
function update() { function update() {
if (body == null) return; if (body == null) return;
var deltaTime:Float = iron.system.Time.delta; var deltaTime:Float = iron.system.Time.delta;
@ -132,14 +136,15 @@ class FirstPersonController extends Trait {
} }
#end #end
// Dejo establecido el salto para tener en cuenta la (enableFatigue) si es que es false/true.... // Dejo establecido el salto para tener en cuenta (isFatigued)
// I set the jump to take into account the (isFatigued)
if (isGrounded && !isFatigued()) { if (isGrounded && !isFatigued()) {
canJump = true; canJump = true;
} }
// Saltar con estamina // Saltar con estamina // Jump with stamina
if (enableJump && kb.started(jumpKey) && canJump) { if (enableJump && kb.started(jumpKey) && canJump) {
var jumpPower = jumpForce; var jumpPower = jumpForce;
// Disminuir el salto al 50% si la (stamina) esta por debajo o en el 20%. // Disminuir el salto al 50% si la (stamina) esta por debajo o en el 20%. // Decrease jump to 50% if stamina is below or at 20%
if (stamina) { if (stamina) {
if (staminaValue <= 0) { if (staminaValue <= 0) {
jumpPower = 0; jumpPower = 0;
@ -158,7 +163,7 @@ class FirstPersonController extends Trait {
} }
} }
// Control de estamina y correr // Control de estamina y correr // Control of stamina and running
if (canRun && kb.down(runKey) && isMoving) { if (canRun && kb.down(runKey) && isMoving) {
if (stamina) { if (stamina) {
if (staminaValue > 0.0) { if (staminaValue > 0.0) {
@ -175,7 +180,7 @@ class FirstPersonController extends Trait {
isRunning = false; isRunning = false;
} }
// (temporizadores aparte) // (temporizadores aparte) (fatigue system timers)
if (isRunning) { if (isRunning) {
timeSinceStop = 0.0; timeSinceStop = 0.0;
fatigueTimer += deltaTime; fatigueTimer += deltaTime;
@ -185,24 +190,25 @@ class FirstPersonController extends Trait {
fatigueCooldown += deltaTime; fatigueCooldown += deltaTime;
} }
// Evitar correr y saltar al estar fatigado... // Evitar correr y saltar al estar fatigado // Avoid running and jumping when fatigued
if (isFatigued()) { if (isFatigued()) {
isRunning = false; isRunning = false;
canJump = false; canJump = false;
} }
// Activar fatiga despues de correr continuamente durante cierto umbral // Activar fatiga despues de correr continuamente durante cierto umbral
// Activate fatigue after running continuously for a certain threshold
if (enableFatigue && fatigueTimer >= fatigueThreshold) { if (enableFatigue && fatigueTimer >= fatigueThreshold) {
isFatigueActive = true; isFatigueActive = true;
} }
// Eliminar la fatiga despues de recuperarse // Eliminar la fatiga despues de recuperarse // Eliminate fatigue after recovery (fatRecoveryThreshold)
if (enableFatigue && isFatigueActive && fatigueCooldown >= fatRecoveryThreshold) { if (enableFatigue && isFatigueActive && fatigueCooldown >= fatRecoveryThreshold) {
isFatigueActive = false; isFatigueActive = false;
fatigueTimer = 0.0; fatigueTimer = 0.0;
} }
// Recuperar estamina si no esta corriendo // Recuperar estamina si no esta corriendo // Recover stamina if you re not running ()
if (stamina && !isRunning && staminaValue < staminaBase && !isFatigued()) { if (stamina && !isRunning && staminaValue < staminaBase && !isFatigued()) {
if (timeSinceStop >= staRecoverTime) { if (timeSinceStop >= staRecoverTime) {
staminaValue += staRecoverPerSec * deltaTime; staminaValue += staRecoverPerSec * deltaTime;
@ -210,7 +216,7 @@ class FirstPersonController extends Trait {
} }
} }
// Movimiento ejes (local) // Movimiento en ejes locales // Movement on local axies
dir.set(0, 0, 0); dir.set(0, 0, 0);
if (moveForward) dir.add(object.transform.look()); if (moveForward) dir.add(object.transform.look());
if (moveBackward) dir.add(object.transform.look().mult(-1)); if (moveBackward) dir.add(object.transform.look().mult(-1));
@ -220,6 +226,7 @@ class FirstPersonController extends Trait {
var btvec = body.getLinearVelocity(); var btvec = body.getLinearVelocity();
body.setLinearVelocity(0.0, 0.0, btvec.z - 1.0); body.setLinearVelocity(0.0, 0.0, btvec.z - 1.0);
// Movement speed control (final) (run and fatigued when are true/false)
if (isMoving) { if (isMoving) {
var dirN = dir.normalize(); var dirN = dir.normalize();
var baseSpeed = moveSpeed; var baseSpeed = moveSpeed;

View File

@ -0,0 +1,278 @@
package leenkx.trait;
import iron.Trait;
import iron.math.Vec4;
import iron.system.Input;
import iron.object.Object;
import iron.object.CameraObject;
import leenkx.trait.physics.PhysicsWorld;
import leenkx.trait.physics.RigidBody;
import kha.System;
class OverheadPersonController extends Trait {
#if (!lnx_physics)
public function new() { super(); }
#else
// Nota: Dejo establecido que el eje (+Y) sera considerada la "cara" del personaje
// I established that the axis (+Y) will be considered the "face" of the character.
// Camara
@prop public var cameraFollow:CameraObject;
@prop public var smoothTrack:Bool = false;
@prop public var smoothSpeed:Float = 5.0;
@prop public var enableJump:Bool = true;
@prop public var jumpForce:Float = 22.0;
@prop public var moveSpeed:Float = 500.0;
@prop public var forwardKey:String = "w";
@prop public var backwardKey:String = "s";
@prop public var leftKey:String = "a";
@prop public var rightKey:String = "d";
@prop public var jumpKey:String = "space";
@prop public var allowAirJump:Bool = false;
@prop public var canRun:Bool = true;
@prop public var runKey:String = "shift";
@prop public var runSpeed:Float = 1000.0;
// Sistema de estamina
@prop public var stamina:Bool = false;
@prop public var staminaBase:Float = 75.0;
@prop public var staRecoverPerSec:Float = 5.0;
@prop public var staDecreasePerSec:Float = 5.0;
@prop public var staRecoverTime:Float = 2.0;
@prop public var staDecreasePerJump:Float = 5.0;
@prop public var enableFatigue:Bool = false;
@prop public var fatigueSpeed:Float = 0.5; // the reduction of movement when fatigue is activated... // reduccion de movimiento con la fatiga activada
@prop public var fatigueThreshold:Float = 30.0; // Tiempo corriendo sin parar para la activacion // Time running non-stop for activation...
@prop public var fatRecoveryThreshold:Float = 7.5; // Tiempo sin correr/saltar para salir de fatiga // Time without running/jumping to get rid of fatigue...
// Variables privadas
var body:RigidBody;
// var de la camara (camera vars)
var initialCameraLoc:Vec4;
var initialOffset:Vec4;
var currentPos:Vec4;
var moveForward:Bool = false;
var moveBackward:Bool = false;
var moveLeft:Bool = false;
var moveRight:Bool = false;
var isRunning:Bool = false;
var canJump:Bool = true;
var staminaValue:Float = 0.0;
var timeSinceStop:Float = 0.0;
var fatigueTimer:Float = 0.0;
var fatigueCooldown:Float = 0.0;
var isFatigueActive:Bool = false;
var dir:Vec4 = new Vec4();
public function new() {
super();
iron.Scene.active.notifyOnInit(init);
}
// Ajustes para la camara y la rotaicon del "jugador"
// Settings for the camera and the "player" rotation
function init() {
body = object.getTrait(RigidBody);
if (cameraFollow == null) {
// Alertar al usuario en caso de no asignar una camara. // Alert the user if a camera is not assigned
trace("[OverheadCameraController] a camera was not assigned to 'cameraFollow'.");
} else {
// Guardar la posicion inicial de la camara // Save the initial position of the camera
initialCameraLoc = cameraFollow.transform.loc.clone();
// Calcular el offset relativo al jugador // Calculate the offset relative to the player
initialOffset = initialCameraLoc.sub(object.transform.loc);
currentPos = initialCameraLoc.clone();
}
PhysicsWorld.active.notifyOnPreUpdate(preUpdate);
notifyOnUpdate(update);
notifyOnRemove(removed);
staminaValue = staminaBase;
}
function removed() {
PhysicsWorld.active.removePreUpdate(preUpdate);
}
function preUpdate() {
if (Input.occupied || body == null) return;
var mouse = Input.getMouse();
var screenW = System.windowWidth();
var screenH = System.windowHeight();
// Posicion relativa del mouse respecto al centro de la pantalla
// Relative position of the mouse with respect to the center of the screen
var mouseXRel = mouse.x - screenW / 2;
var mouseYRel = mouse.y - screenH / 2;
// Angulo 360° usando atan2 (invertido para corregir direccion) // 360° angle using atan2 (inverted to correct direction)
var angleZ = -Math.atan2(mouseXRel, -mouseYRel);
object.transform.setRotation(object.transform.rot.x, object.transform.rot.y, angleZ);
body.syncTransform();
// Camara siguiendo al jugador (manteniendo offset inicial) // Camera following the player (maintaining initial offset)
if (cameraFollow != null) {
var playerPos = object.transform.loc;
var targetPos = new Vec4(
playerPos.x + initialOffset.x,
playerPos.y + initialOffset.y,
playerPos.z + initialOffset.z,
1 // w 1 (posicion absoluta)
// used w=1 to indicate that the camera position is an absolute point in the world...
);
// Mover la camara a 'targetPos' de forma gradual o instantanea si 'smoothTrack' es false
// Moves the camera to 'targetPos' gradually or instantly if 'smoothTrack' is false
if (smoothTrack) {
var delta = targetPos.sub(currentPos);
var moveStep = delta.mult(smoothSpeed * iron.system.Time.delta);
if (moveStep.length() > delta.length()) moveStep = delta;
currentPos = currentPos.add(moveStep);
cameraFollow.transform.loc.setFrom(currentPos);
} else {
currentPos = targetPos.clone();
cameraFollow.transform.loc.setFrom(currentPos);
}
}
}
function isFatigued():Bool {
return enableFatigue && isFatigueActive;
}
function update() {
if (body == null) return;
var deltaTime:Float = iron.system.Time.delta;
var kb = Input.getKeyboard();
moveForward = kb.down(forwardKey);
moveBackward = kb.down(backwardKey);
moveLeft = kb.down(leftKey);
moveRight = kb.down(rightKey);
var isMoving = moveForward || moveBackward || moveLeft || moveRight;
var isGrounded:Bool = false;
#if lnx_physics
var vel = body.getLinearVelocity();
if (Math.abs(vel.z) < 0.1) isGrounded = true;
#end
// Dejo establecido el salto para tener en cuenta (isFatigued)
// I set the jump to take into account the (isFatigued)
if (isGrounded && !isFatigued()) {
canJump = true;
}
// Saltar con estamina // Jump with stamina
if (enableJump && kb.started(jumpKey) && canJump) {
var jumpPower = jumpForce;
// Disminuir el salto al 50% si la (stamina) esta por debajo o en el 20%. // Decrease jump to 50% if stamina is below or at 20%
if (stamina) {
if (staminaValue <= 0) {
jumpPower = 0;
} else if (staminaValue <= staminaBase * 0.2) {
jumpPower *= 0.5;
}
staminaValue -= staDecreasePerJump;
if (staminaValue < 0.0) staminaValue = 0.0;
timeSinceStop = 0.0;
}
if (jumpPower > 0) {
body.applyImpulse(new Vec4(0, 0, jumpPower));
if (!allowAirJump) canJump = false;
}
}
// Control de estamina y correr // Control of stamina and running
if (canRun && kb.down(runKey) && isMoving) {
if (stamina) {
if (staminaValue > 0.0) {
isRunning = true;
staminaValue -= staDecreasePerSec * deltaTime;
if (staminaValue < 0.0) staminaValue = 0.0;
} else {
isRunning = false;
}
} else {
isRunning = true;
}
} else {
isRunning = false;
}
// (temporizadores aparte) (fatigue system timers)
if (isRunning) {
timeSinceStop = 0.0;
fatigueTimer += deltaTime;
fatigueCooldown = 0.0;
} else {
timeSinceStop += deltaTime;
fatigueCooldown += deltaTime;
}
// Evitar correr y saltar al estar fatigado // Avoid running and jumping when fatigued
if (isFatigued()) {
isRunning = false;
canJump = false;
}
// Activar fatiga despues de correr continuamente durante cierto umbral
// Activate fatigue after running continuously for a certain threshold
if (enableFatigue && fatigueTimer >= fatigueThreshold) {
isFatigueActive = true;
}
// Eliminar la fatiga despues de recuperarse // Eliminate fatigue after recovery (fatRecoveryThreshold)
if (enableFatigue && isFatigueActive && fatigueCooldown >= fatRecoveryThreshold) {
isFatigueActive = false;
fatigueTimer = 0.0;
}
// Recuperar estamina si no esta corriendo // Recover stamina if you re not running ()
if (stamina && !isRunning && staminaValue < staminaBase && !isFatigued()) {
if (timeSinceStop >= staRecoverTime) {
staminaValue += staRecoverPerSec * deltaTime;
if (staminaValue > staminaBase) staminaValue = staminaBase;
}
}
// Movimiento en ejes globales // Movement on global axies
dir.set(0,0,0);
if (moveForward) dir.add(new Vec4(0, 1, 0, 0));
if (moveBackward) dir.add(new Vec4(0,-1, 0, 0));
if (moveRight) dir.add(new Vec4(1, 0, 0, 0));
if (moveLeft) dir.add(new Vec4(-1,0, 0, 0));
var btvec = body.getLinearVelocity();
body.setLinearVelocity(0.0, 0.0, btvec.z - 1.0);
// Movement speed control (final) (run and fatigued when are true/false)
if (dir.length() > 0) {
var dirN = dir.normalize();
var baseSpeed = moveSpeed;
if (isRunning) baseSpeed = runSpeed;
var currentSpeed = isFatigued() ? baseSpeed * fatigueSpeed : baseSpeed;
dirN.mult(currentSpeed * deltaTime);
body.activate();
body.setLinearVelocity(dirN.x, dirN.y, btvec.z - 1.0);
}
body.setAngularFactor(0,0,0);
if (cameraFollow != null) cameraFollow.buildMatrix();
}
#end
}
// Stamina and fatigue system.....

View File

@ -6,60 +6,91 @@ import iron.Trait;
import iron.object.MeshObject; import iron.object.MeshObject;
import iron.data.MeshData; import iron.data.MeshData;
import iron.data.SceneFormat; import iron.data.SceneFormat;
#if lnx_bullet #if (lnx_bullet || lnx_oimo)
import leenkx.trait.physics.bullet.RigidBody; import leenkx.trait.physics.RigidBody;
import leenkx.trait.physics.PhysicsWorld; import leenkx.trait.physics.PhysicsWorld;
#end #end
class PhysicsBreak extends Trait { class PhysicsBreak extends Trait {
#if (!lnx_bullet) #if (!lnx_bullet && !lnx_oimo)
public function new() { super(); } public function new() { super(); }
#else #else
static var physics: PhysicsWorld = null; // Track all debris for cleanup on scene change
static var breaker: ConvexBreaker = null; static var allDebris: Array<MeshObject> = [];
static var sceneCallbackRegistered = false;
var breaker: ConvexBreaker;
var physics: PhysicsWorld;
var body: RigidBody; var body: RigidBody;
public function new() { public function new() {
super(); super();
if (breaker == null) breaker = new ConvexBreaker(); breaker = new ConvexBreaker();
notifyOnInit(init); notifyOnInit(init);
} }
function init() { function init() {
if (physics == null) physics = leenkx.trait.physics.PhysicsWorld.active; physics = leenkx.trait.physics.PhysicsWorld.active;
if (physics == null) return;
body = object.getTrait(RigidBody); body = object.getTrait(RigidBody);
breaker.initBreakableObject(cast object, body.mass, body.friction, new Vec4(), new Vec4(), true); breaker.initBreakableObject(cast object, body.mass, body.friction, new Vec4(), new Vec4(), true);
// Register scene removal callback once
if (!sceneCallbackRegistered) {
sceneCallbackRegistered = true;
iron.Scene.active.notifyOnRemove(cleanupAllDebris);
}
notifyOnUpdate(update); notifyOnUpdate(update);
} }
static function cleanupAllDebris() {
// Make a copy since remove() modifies the array
var toRemove = allDebris.copy();
for (debris in toRemove) {
if (debris != null) debris.remove();
}
allDebris = [];
sceneCallbackRegistered = false;
}
function update() { function update() {
if (body == null || !body.ready || physics == null) return;
var ar = physics.getContactPairs(body); var ar = physics.getContactPairs(body);
if (ar != null) { if (ar != null) {
var maxImpulse = 0.0; var maxImpulse: Float = 0.0;
var impactPoint: Vec4 = null; var impactPoint: Vec4 = null;
var impactNormal: Vec4 = null; var impactNormal: Vec4 = null;
for (p in ar) { for (p in ar) {
if (maxImpulse < p.impulse) { if (maxImpulse < p.impulse) {
maxImpulse = p.impulse; maxImpulse = p.impulse;
impactPoint = p.posB; impactPoint = p.posB;
#if lnx_bullet
impactNormal = p.normOnB; impactNormal = p.normOnB;
#elseif lnx_oimo
impactNormal = p.nor;
#end
} }
} }
#if lnx_bullet
var fractureImpulse = 4.0; var fractureImpulse = 4.0;
if (maxImpulse > fractureImpulse) { #elseif lnx_oimo
var fractureImpulse = 1.0;
#end
if (maxImpulse > fractureImpulse && impactPoint != null && impactNormal != null) {
var radialIter = 1; var radialIter = 1;
var randIter = 1; var randIter = 1;
var debris = breaker.subdivideByImpact(cast object, impactPoint, impactNormal, radialIter, randIter); var debris = breaker.subdivideByImpact(cast object, impactPoint, impactNormal, radialIter, randIter);
// var numObjects = debris.length; // var numObjects = debris.length;
for (o in debris) { for (o in debris) {
var ud = breaker.userDataMap.get(cast o); var ud = breaker.userDataMap.get(cast o);
if (ud == null) continue;
var params: RigidBodyParams = { var params: RigidBodyParams = {
linearDamping: 0.04, linearDamping: 0.04,
angularDamping: 0.1, angularDamping: 0.1,
@ -72,7 +103,7 @@ class PhysicsBreak extends Trait {
angularFactorsZ: 1.0, angularFactorsZ: 1.0,
collisionMargin: 0.04, collisionMargin: 0.04,
linearDeactivationThreshold: 0.0, linearDeactivationThreshold: 0.0,
angularDeactivationThrshold: 0.0, angularDeactivationThreshold: 0.0,
deactivationTime: 0.0, deactivationTime: 0.0,
linearVelocityMin: 0.0, linearVelocityMin: 0.0,
linearVelocityMax: 0.0, linearVelocityMax: 0.0,
@ -89,7 +120,12 @@ class PhysicsBreak extends Trait {
if (cast(o, MeshObject).data.geom.positions.values.length < 600) { if (cast(o, MeshObject).data.geom.positions.values.length < 600) {
o.addTrait(new PhysicsBreak()); o.addTrait(new PhysicsBreak());
} }
// Track debris for cleanup on scene change
allDebris.push(cast o);
} }
// Remove self from update before removing object
remove();
object.remove(); object.remove();
} }
} }
@ -485,6 +521,7 @@ class ConvexBreaker {
var numObjects = 0; var numObjects = 0;
if (numPoints1 > 4) { if (numPoints1 > 4) {
var data1 = makeMeshData(points1); var data1 = makeMeshData(points1);
if (data1 != null) {
object1 = new MeshObject(data1, object.materials); object1 = new MeshObject(data1, object.materials);
object1.transform.loc.setFrom(tempCM1); object1.transform.loc.setFrom(tempCM1);
object1.transform.rot.setFrom(object.transform.rot); object1.transform.rot.setFrom(object.transform.rot);
@ -492,9 +529,11 @@ class ConvexBreaker {
initBreakableObject(object1, newMass, userData.friction, userData.velocity, userData.angularVelocity, 2 * radius1 > minSizeForBreak); initBreakableObject(object1, newMass, userData.friction, userData.velocity, userData.angularVelocity, 2 * radius1 > minSizeForBreak);
numObjects++; numObjects++;
} }
}
if (numPoints2 > 4) { if (numPoints2 > 4) {
var data2 = makeMeshData(points2); var data2 = makeMeshData(points2);
if (data2 != null) {
object2 = new MeshObject(data2, object.materials); object2 = new MeshObject(data2, object.materials);
object2.transform.loc.setFrom(tempCM2); object2.transform.loc.setFrom(tempCM2);
object2.transform.rot.setFrom(object.transform.rot); object2.transform.rot.setFrom(object.transform.rot);
@ -502,6 +541,7 @@ class ConvexBreaker {
initBreakableObject(object2, newMass, userData.friction, userData.velocity, userData.angularVelocity, 2 * radius2 > minSizeForBreak); initBreakableObject(object2, newMass, userData.friction, userData.velocity, userData.angularVelocity, 2 * radius2 > minSizeForBreak);
numObjects++; numObjects++;
} }
}
output.object1 = object1; output.object1 = object1;
output.object2 = object2; output.object2 = object2;
@ -510,9 +550,15 @@ class ConvexBreaker {
static var meshIndex = 0; static var meshIndex = 0;
function makeMeshData(points: Array<Vec4>): MeshData { function makeMeshData(points: Array<Vec4>): MeshData {
// Need at least 4 points for a 3D hull
if (points.length < 4) return null;
while (points.length > 50) points.pop(); while (points.length > 50) points.pop();
var cm = new ConvexHull(points); var cm = new ConvexHull(points);
// Validate hull has enough geometry for a mesh
if (cm.vertices.length < 4 || cm.face3s.length < 4) return null;
var maxdim = 1.0; var maxdim = 1.0;
var pa = new Array<Float>(); var pa = new Array<Float>();
var na = new Array<Float>(); var na = new Array<Float>();

View File

@ -271,7 +271,7 @@ class DebugConsole extends Trait {
if (ui.tab(htab, "Scene")) { if (ui.tab(htab, "Scene")) {
if (ui.panel(Id.handle({selected: true}), "Outliner: obj.uid_obj.name")) { if (ui.panel(Id.handle({selected: true}), "Outliner: [ obj.uid ] : obj.name")) {
ui.indent(); ui.indent();
ui._y -= ui.ELEMENT_OFFSET(); ui._y -= ui.ELEMENT_OFFSET();
@ -282,9 +282,9 @@ class DebugConsole extends Trait {
var _y = ui._y; var _y = ui._y;
if (object.parent.name == 'Root' && object.raw == null) if (object.parent.name == 'Root' && object.raw == null)
ui.text(object.uid+'_'+object.name+' ('+iron.Scene.active.raw.world_ref+')'); ui.text('[ '+object.uid+' ] : '+object.name+' ( '+iron.Scene.active.raw.world_ref+' )');
else else
ui.text(object.uid+'_'+object.name); ui.text('[ '+object.uid+' ] : '+object.name);
if (object == iron.Scene.active.camera) { if (object == iron.Scene.active.camera) {
var tagWidth = 100; var tagWidth = 100;

View File

@ -3,7 +3,7 @@ package leenkx.trait.physics;
#if (!lnx_physics) #if (!lnx_physics)
class PhysicsConstraint extends iron.Trait { public function new() { super(); } } class PhysicsConstraint extends iron.Trait { public function new() { super(); } }
@:enum abstract ConstraintAxis(Int) from Int to Int { } enum abstract ConstraintAxis(Int) from Int to Int { }
#else #else

View File

@ -3,19 +3,22 @@ package leenkx.trait.physics;
#if (!lnx_physics) #if (!lnx_physics)
class RigidBody extends iron.Trait { public function new() { super(); } } class RigidBody extends iron.Trait { public function new() { super(); } }
@:enum abstract Shape(Int) from Int to Int { } enum abstract Shape(Int) from Int to Int { }
#else #else
#if lnx_bullet #if lnx_bullet
typedef RigidBody = leenkx.trait.physics.bullet.RigidBody; typedef RigidBody = leenkx.trait.physics.bullet.RigidBody;
typedef Shape = leenkx.trait.physics.bullet.RigidBody.Shape; typedef Shape = leenkx.trait.physics.bullet.RigidBody.Shape;
typedef RigidBodyParams = leenkx.trait.physics.bullet.RigidBody.RigidBodyParams;
#elseif lnx_jolt #elseif lnx_jolt
typedef RigidBody = leenkx.trait.physics.jolt.RigidBody; typedef RigidBody = leenkx.trait.physics.jolt.RigidBody;
typedef Shape = leenkx.trait.physics.jolt.RigidBody.Shape; typedef Shape = leenkx.trait.physics.jolt.RigidBody.Shape;
typedef RigidBodyParams = leenkx.trait.physics.jolt.RigidBody.RigidBodyParams;
#else #else
typedef RigidBody = leenkx.trait.physics.oimo.RigidBody; typedef RigidBody = leenkx.trait.physics.oimo.RigidBody;
typedef Shape = leenkx.trait.physics.oimo.RigidBody.Shape; typedef Shape = leenkx.trait.physics.oimo.RigidBody.Shape;
typedef RigidBodyParams = leenkx.trait.physics.oimo.RigidBody.RigidBodyParams;
#end #end
#end #end

View File

@ -346,7 +346,7 @@ class KinematicCharacterController extends Trait {
} }
} }
@:enum abstract ControllerShape(Int) from Int to Int { enum abstract ControllerShape(Int) from Int to Int {
var Box = 0; var Box = 0;
var Sphere = 1; var Sphere = 1;
var ConvexHull = 2; var ConvexHull = 2;
@ -355,7 +355,7 @@ class KinematicCharacterController extends Trait {
var Capsule = 5; var Capsule = 5;
} }
@:enum abstract ControllerActivationState(Int) from Int to Int { enum abstract ControllerActivationState(Int) from Int to Int {
var Active = 1; var Active = 1;
var NoDeactivation = 4; var NoDeactivation = 4;
var NoSimulation = 5; var NoSimulation = 5;

View File

@ -453,10 +453,9 @@ class PhysicsConstraint extends iron.Trait {
#end #end
} }
} }
@:enum abstract ConstraintType(Int) from Int to Int { enum abstract ConstraintType(Int) from Int to Int {
var Fixed = 0; var Fixed = 0;
var Point = 1; var Point = 1;
var Hinge = 2; var Hinge = 2;
@ -467,7 +466,7 @@ class PhysicsConstraint extends iron.Trait {
var Motor = 7; var Motor = 7;
} }
@:enum abstract ConstraintAxis(Int) from Int to Int { enum abstract ConstraintAxis(Int) from Int to Int {
var X = 0; var X = 0;
var Y = 1; var Y = 1;
var Z = 2; var Z = 2;

View File

@ -1,7 +1,6 @@
package leenkx.trait.physics.bullet; package leenkx.trait.physics.bullet;
#if lnx_bullet #if lnx_bullet
import iron.Trait; import iron.Trait;
import iron.system.Time; import iron.system.Time;
import iron.math.Vec4; import iron.math.Vec4;
@ -10,7 +9,6 @@ import iron.math.RayCaster;
import leenkx.trait.physics.PhysicsCache; import leenkx.trait.physics.PhysicsCache;
class Hit { class Hit {
public var rb: RigidBody; public var rb: RigidBody;
public var pos: Vec4; public var pos: Vec4;
public var normal: Vec4; public var normal: Vec4;
@ -33,7 +31,6 @@ class ConvexHit {
} }
class ContactPair { class ContactPair {
public var a: Int; public var a: Int;
public var b: Int; public var b: Int;
public var posA: Vec4; public var posA: Vec4;
@ -48,7 +45,6 @@ class ContactPair {
} }
class PhysicsWorld extends Trait { class PhysicsWorld extends Trait {
public static var active: PhysicsWorld = null; public static var active: PhysicsWorld = null;
static var sceneRemoved = false; static var sceneRemoved = false;
@ -207,6 +203,12 @@ class PhysicsWorld extends Trait {
rbMap.set(body.id, body); rbMap.set(body.id, body);
} }
public function updateRigidBody(body: RigidBody) {
if (world != null) world.removeRigidBody(body.body);
rbMap.remove(body.id);
addRigidBody(body);
}
public function addPhysicsConstraint(constraint: PhysicsConstraint) { public function addPhysicsConstraint(constraint: PhysicsConstraint) {
world.addConstraint(constraint.con, constraint.disableCollisions); world.addConstraint(constraint.con, constraint.disableCollisions);
conMap.set(constraint.id, constraint); conMap.set(constraint.id, constraint);
@ -319,7 +321,7 @@ class PhysicsWorld extends Trait {
world.stepSimulation(t, currMaxSteps, Time.fixedStep); world.stepSimulation(t, currMaxSteps, Time.fixedStep);
updateContacts(); updateContacts();
for (rb in rbMap) @:privateAccess rb.physicsUpdate(); for (rb in rbMap) { @:privateAccess try { rb.physicsUpdate(); } catch(e:haxe.Exception) { trace(e.message); } } // HACK: see this recommendation: https://github.com/armory3d/armory/issues/3044#issuecomment-2558199944.
#if lnx_debug #if lnx_debug
physTime = kha.Scheduler.realTime() - startTime; physTime = kha.Scheduler.realTime() - startTime;

View File

@ -68,6 +68,10 @@ class RigidBody extends iron.Trait {
public var onReady: Void->Void = null; public var onReady: Void->Void = null;
public var onContact: Array<RigidBody->Void> = null; public var onContact: Array<RigidBody->Void> = null;
public var heightData: haxe.io.Bytes = null; public var heightData: haxe.io.Bytes = null;
// Compound shape children (baked from exporter)
var compoundChildren: Array<CompoundChild> = null;
#if js #if js
static var ammoArray: Int = -1; static var ammoArray: Int = -1;
#end #end
@ -92,7 +96,6 @@ class RigidBody extends iron.Trait {
// Interpolation // Interpolation
var interpolate: Bool = false; var interpolate: Bool = false;
var time: Float = 0.0;
var currentPos: bullet.Bt.Vector3 = new bullet.Bt.Vector3(0, 0, 0); var currentPos: bullet.Bt.Vector3 = new bullet.Bt.Vector3(0, 0, 0);
var prevPos: bullet.Bt.Vector3 = new bullet.Bt.Vector3(0, 0, 0); var prevPos: bullet.Bt.Vector3 = new bullet.Bt.Vector3(0, 0, 0);
var currentRot: bullet.Bt.Quaternion = new bullet.Bt.Quaternion(0, 0, 0, 1); var currentRot: bullet.Bt.Quaternion = new bullet.Bt.Quaternion(0, 0, 0, 1);
@ -131,7 +134,7 @@ class RigidBody extends iron.Trait {
angularFactorsZ: 1.0, angularFactorsZ: 1.0,
collisionMargin: 0.0, collisionMargin: 0.0,
linearDeactivationThreshold: 0.0, linearDeactivationThreshold: 0.0,
angularDeactivationThrshold: 0.0, angularDeactivationThreshold: 0.0,
deactivationTime: 0.0, deactivationTime: 0.0,
linearVelocityMin: 0.0, linearVelocityMin: 0.0,
linearVelocityMax: 0.0, linearVelocityMax: 0.0,
@ -160,7 +163,7 @@ class RigidBody extends iron.Trait {
this.linearFactors = [params.linearFactorsX, params.linearFactorsY, params.linearFactorsZ]; this.linearFactors = [params.linearFactorsX, params.linearFactorsY, params.linearFactorsZ];
this.angularFactors = [params.angularFactorsX, params.angularFactorsY, params.angularFactorsZ]; this.angularFactors = [params.angularFactorsX, params.angularFactorsY, params.angularFactorsZ];
this.collisionMargin = params.collisionMargin; this.collisionMargin = params.collisionMargin;
this.deactivationParams = [params.linearDeactivationThreshold, params.angularDeactivationThrshold, params.deactivationTime]; this.deactivationParams = [params.linearDeactivationThreshold, params.angularDeactivationThreshold, params.deactivationTime];
// New velocity limiting properties // New velocity limiting properties
this.linearVelocityMin = params.linearVelocityMin; this.linearVelocityMin = params.linearVelocityMin;
this.linearVelocityMax = params.linearVelocityMax; this.linearVelocityMax = params.linearVelocityMax;
@ -180,6 +183,9 @@ class RigidBody extends iron.Trait {
this.staticObj = flags.staticObj; this.staticObj = flags.staticObj;
this.useDeactivation = flags.useDeactivation; this.useDeactivation = flags.useDeactivation;
// Store compound children data if provided
this.compoundChildren = params.compoundChildren;
notifyOnAdd(init); notifyOnAdd(init);
} }
@ -281,6 +287,25 @@ class RigidBody extends iron.Trait {
btshape.setLocalScaling(vec1); btshape.setLocalScaling(vec1);
#end #end
} }
else if (shape == Shape.Compound) {
// Create compound shape and add all child shapes
var compound = new bullet.Bt.CompoundShape(true);
if (compoundChildren != null) {
for (child in compoundChildren) {
var childShape = createChildShape(child);
if (childShape != null) {
// Set child local transform
trans2.setIdentity();
vec1.setValue(child.posX, child.posY, child.posZ);
trans2.setOrigin(vec1);
quat1.setValue(child.rotX, child.rotY, child.rotZ, child.rotW);
trans2.setRotation(quat1);
compound.addChildShape(trans2, childShape);
}
}
}
btshape = compound;
}
trans1.setIdentity(); trans1.setIdentity();
vec1.setX(transform.worldx()); vec1.setX(transform.worldx());
@ -382,16 +407,49 @@ class RigidBody extends iron.Trait {
#end #end
} }
/**
* Creates a child collision shape for compound rigidbodies from baked export data.
* @param child The compound child data containing shape type and dimensions
* @return The created Bullet collision shape, or null if shape type is unsupported
*/
function createChildShape(child: CompoundChild): bullet.Bt.CollisionShape {
var childShapeType: Int = child.shape;
if (childShapeType == Shape.Box) {
vec1.setValue(withMargin(child.dimX / 2), withMargin(child.dimY / 2), withMargin(child.dimZ / 2));
return new bullet.Bt.BoxShape(vec1);
}
else if (childShapeType == Shape.Sphere) {
return new bullet.Bt.SphereShape(withMargin(child.dimX / 2));
}
else if (childShapeType == Shape.Cone) {
var coneZ = new bullet.Bt.ConeShapeZ(
withMargin(child.dimX / 2), // Radius
withMargin(child.dimZ)); // Height
return coneZ;
}
else if (childShapeType == Shape.Cylinder) {
vec1.setValue(withMargin(child.dimX / 2), withMargin(child.dimY / 2), withMargin(child.dimZ / 2));
var cylZ = new bullet.Bt.CylinderShapeZ(vec1);
return cylZ;
}
else if (childShapeType == Shape.Capsule) {
var r = child.dimX / 2;
var capsZ = new bullet.Bt.CapsuleShapeZ(
withMargin(r), // Radius
withMargin(child.dimZ - r * 2)); // Height between 2 sphere centers
return capsZ;
}
else {
// Unsupported shape type for compound children (ConvexHull, Mesh, Terrain)
trace("Warning: Unsupported compound child shape type: " + childShapeType);
return null;
}
}
function update() { function update() {
if (interpolate) { if (interpolate) {
time += Time.delta; var t: Float = Time.fixedStepInterpolation;
while (time >= Time.fixedStep) {
time -= Time.fixedStep;
}
var t: Float = time / Time.fixedStep;
t = Helper.clamp(t, 0, 1); t = Helper.clamp(t, 0, 1);
var tx: Float = prevPos.x() * (1.0 - t) + currentPos.x() * t; var tx: Float = prevPos.x() * (1.0 - t) + currentPos.x() * t;
@ -402,9 +460,6 @@ class RigidBody extends iron.Trait {
transform.loc.set(tx, ty, tz, 1.0); transform.loc.set(tx, ty, tz, 1.0);
transform.rot.set(tRot.x(), tRot.y(), tRot.z(), tRot.w()); transform.rot.set(tRot.x(), tRot.y(), tRot.z(), tRot.w());
} else {
transform.loc.set(currentPos.x(), currentPos.y(), currentPos.z(), 1.0);
transform.rot.set(currentRot.x(), currentRot.y(), currentRot.z(), currentRot.w());
} }
if (object.parent != null) { if (object.parent != null) {
@ -432,28 +487,26 @@ class RigidBody extends iron.Trait {
function physicsUpdate() { function physicsUpdate() {
if (!ready) return; if (!ready) return;
if (animated) {
syncTransform();
} else {
if (interpolate) {
prevPos.setValue(currentPos.x(), currentPos.y(), currentPos.z()); prevPos.setValue(currentPos.x(), currentPos.y(), currentPos.z());
prevRot.setValue(currentRot.x(), currentRot.y(), currentRot.z(), currentRot.w()); prevRot.setValue(currentRot.x(), currentRot.y(), currentRot.z(), currentRot.w());
if (animated) {
syncTransform();
} }
var trans = body.getWorldTransform(); var trans = body.getWorldTransform();
var p = trans.getOrigin(); var p = trans.getOrigin();
var q = trans.getRotation(); var q = trans.getRotation();
transform.clearDelta();
// transform.buildMatrix();
currentPos.setValue(p.x(), p.y(), p.z()); currentPos.setValue(p.x(), p.y(), p.z());
currentRot.setValue(q.x(), q.y(), q.z(), q.w()); currentRot.setValue(q.x(), q.y(), q.z(), q.w());
#if hl #if hl
p.delete(); p.delete();
q.delete(); q.delete();
trans.delete(); trans.delete();
#end #end
}
if (onContact != null) { if (onContact != null) {
var rbs = physics.getContacts(this); var rbs = physics.getContacts(this);
@ -533,6 +586,18 @@ class RigidBody extends iron.Trait {
bodyColl.activate(false); bodyColl.activate(false);
} }
public function setGroup(group: Int) {
if (this.group == group) return;
this.group = group;
physics.updateRigidBody(this);
}
public function setMask(mask: Int) {
if (this.mask == mask) return;
this.mask = mask;
physics.updateRigidBody(this);
}
public function disableGravity() { public function disableGravity() {
vec1.setValue(0, 0, 0); vec1.setValue(0, 0, 0);
body.setGravity(vec1); body.setGravity(vec1);
@ -817,7 +882,7 @@ class RigidBody extends iron.Trait {
} }
} }
@:enum abstract Shape(Int) from Int to Int { enum abstract Shape(Int) from Int to Int {
var Box = 0; var Box = 0;
var Sphere = 1; var Sphere = 1;
var ConvexHull = 2; var ConvexHull = 2;
@ -826,6 +891,7 @@ class RigidBody extends iron.Trait {
var Cylinder = 5; var Cylinder = 5;
var Capsule = 6; var Capsule = 6;
var Terrain = 7; var Terrain = 7;
var Compound = 8;
} }
typedef RigidBodyParams = { typedef RigidBodyParams = {
@ -840,7 +906,7 @@ typedef RigidBodyParams = {
var angularFactorsZ: Float; var angularFactorsZ: Float;
var collisionMargin: Float; var collisionMargin: Float;
var linearDeactivationThreshold: Float; var linearDeactivationThreshold: Float;
var angularDeactivationThrshold: Float; var angularDeactivationThreshold: Float;
var deactivationTime: Float; var deactivationTime: Float;
var linearVelocityMin: Float; var linearVelocityMin: Float;
var linearVelocityMax: Float; var linearVelocityMax: Float;
@ -852,6 +918,21 @@ typedef RigidBodyParams = {
var lockRotationX: Bool; var lockRotationX: Bool;
var lockRotationY: Bool; var lockRotationY: Bool;
var lockRotationZ: Bool; var lockRotationZ: Bool;
@:optional var compoundChildren: Array<CompoundChild>;
}
typedef CompoundChild = {
var shape: Int; // 0=Box, 1=Sphere, 2=ConvexHull, 3=Mesh, 4=Cone, 5=Cylinder, 6=Capsule
var posX: Float; // Local position relative to parent
var posY: Float;
var posZ: Float;
var rotX: Float; // Local rotation quaternion
var rotY: Float;
var rotZ: Float;
var rotW: Float;
var dimX: Float; // Dimensions
var dimY: Float;
var dimZ: Float;
} }
typedef RigidBodyFlags = { typedef RigidBodyFlags = {

View File

@ -423,7 +423,7 @@ class SoftBody extends Trait {
#end #end
} }
@:enum abstract SoftShape(Int) from Int { enum abstract SoftShape(Int) from Int {
var Cloth = 0; var Cloth = 0;
var Volume = 1; var Volume = 1;
} }

View File

@ -99,7 +99,7 @@ class RigidBody extends Trait {
angularFactorsZ: 1.0, angularFactorsZ: 1.0,
collisionMargin: 0.0, collisionMargin: 0.0,
linearDeactivationThreshold: 0.0, linearDeactivationThreshold: 0.0,
angularDeactivationThrshold: 0.0, angularDeactivationThreshold: 0.0,
deactivationTime: 0.0, deactivationTime: 0.0,
linearVelocityMin: 0.0, linearVelocityMin: 0.0,
linearVelocityMax: 0.0, linearVelocityMax: 0.0,
@ -152,6 +152,7 @@ class RigidBody extends Trait {
return; return;
transform = object.transform; transform = object.transform;
transform.buildMatrix();
physics = PhysicsWorld.active; physics = PhysicsWorld.active;
if (physics == null) { if (physics == null) {
@ -720,7 +721,7 @@ typedef RigidBodyParams = {
var angularFactorsZ:Float; var angularFactorsZ:Float;
var collisionMargin:Float; var collisionMargin:Float;
var linearDeactivationThreshold:Float; var linearDeactivationThreshold:Float;
var angularDeactivationThrshold:Float; var angularDeactivationThreshold:Float;
var deactivationTime:Float; var deactivationTime:Float;
var linearVelocityMin:Float; var linearVelocityMin:Float;
var linearVelocityMax:Float; var linearVelocityMax:Float;

View File

@ -483,7 +483,7 @@ typedef TTranslatedText = {
var text: String; var text: String;
} }
@:enum abstract ElementType(Int) from Int to Int { enum abstract ElementType(Int) from Int to Int {
var Text = 0; var Text = 0;
var Image = 1; var Image = 1;
var Button = 2; var Button = 2;
@ -507,7 +507,7 @@ typedef TTranslatedText = {
var TextArea = 20; var TextArea = 20;
} }
@:enum abstract Anchor(Int) from Int to Int { enum abstract Anchor(Int) from Int to Int {
var TopLeft = 0; var TopLeft = 0;
var Top = 1; var Top = 1;
var TopRight = 2; var TopRight = 2;

View File

@ -73,7 +73,7 @@ typedef TTheme = {
var FULL_TABS: Bool; // Make tabs take full window width var FULL_TABS: Bool; // Make tabs take full window width
} }
@:enum abstract LinkStyle(Int) from Int { enum abstract LinkStyle(Int) from Int {
var Line = 0; var Line = 0;
var CubicBezier = 1; var CubicBezier = 1;
} }

View File

@ -1187,14 +1187,22 @@ class Zui {
} }
if (submitTextHandle == handle) { if (submitTextHandle == handle) {
submitTextEdit(); submitTextEdit();
var previousValue: Float = handle.value;
#if js #if js
try { try {
handle.value = js.Lib.eval(handle.text); var evalResult: Dynamic = js.Lib.eval(handle.text);
var parsedValue: Float = Std.parseFloat(Std.string(evalResult));
handle.value = Math.isNaN(parsedValue) ? previousValue : parsedValue;
}
catch(_) {
handle.value = previousValue;
} }
catch(_) {}
#else #else
handle.value = Std.parseFloat(handle.text); var parsedValue: Float = Std.parseFloat(handle.text);
handle.value = Math.isNaN(parsedValue) ? previousValue : parsedValue;
#end #end
if (handle.value < from) handle.value = from;
else if (handle.value > to) handle.value = to;
handle.changed = changed = true; handle.changed = changed = true;
} }
@ -2015,6 +2023,8 @@ typedef HandleOptions = {
} }
class Handle { class Handle {
static var ptrCounter: Int = 0;
public var ptr(default, null): Int; // Unique handle identifier
public var selected = false; public var selected = false;
public var position = 0; public var position = 0;
public var color = kha.Color.White; public var color = kha.Color.White;
@ -2034,6 +2044,7 @@ class Handle {
var children: Map<Int, Handle>; var children: Map<Int, Handle>;
public function new(ops: HandleOptions = null) { public function new(ops: HandleOptions = null) {
ptr = ptrCounter++;
if (ops != null) { if (ops != null) {
if (ops.selected != null) selected = ops.selected; if (ops.selected != null) selected = ops.selected;
if (ops.position != null) position = ops.position; if (ops.position != null) position = ops.position;
@ -2063,18 +2074,18 @@ class Handle {
public static var global = new Handle(); public static var global = new Handle();
} }
@:enum abstract Layout(Int) from Int { enum abstract Layout(Int) from Int {
var Vertical = 0; var Vertical = 0;
var Horizontal = 1; var Horizontal = 1;
} }
@:enum abstract Align(Int) from Int { enum abstract Align(Int) from Int {
var Left = 0; var Left = 0;
var Center = 1; var Center = 1;
var Right = 2; var Right = 2;
} }
@:enum abstract State(Int) from Int { enum abstract State(Int) from Int {
var Idle = 0; var Idle = 0;
var Started = 1; var Started = 1;
var Down = 2; var Down = 2;

View File

@ -55,6 +55,16 @@ def reset():
shader_cons['voxel_frag'] = [] shader_cons['voxel_frag'] = []
shader_cons['voxel_geom'] = [] shader_cons['voxel_geom'] = []
def reset_shader_cons():
# Reset shader comparison arrays to prevent cross-scene shader merging
global shader_cons
shader_cons['mesh_vert'] = []
shader_cons['depth_vert'] = []
shader_cons['depth_frag'] = []
shader_cons['voxel_vert'] = []
shader_cons['voxel_frag'] = []
shader_cons['voxel_geom'] = []
def add(asset_file): def add(asset_file):
global assets global assets

View File

@ -12,6 +12,7 @@ Attribution-ShareAlike 3.0 Unported License:
https://creativecommons.org/licenses/by-sa/3.0/deed.en_US https://creativecommons.org/licenses/by-sa/3.0/deed.en_US
""" """
from enum import Enum, unique from enum import Enum, unique
import copy
import math import math
import os import os
import time import time
@ -32,6 +33,7 @@ from mathutils import Matrix, Vector
import bmesh import bmesh
import lnx.utils import lnx.utils
import lnx.linked_utils as linked_utils
import lnx.profiler import lnx.profiler
from lnx import assets, exporter_opt, log, make_renderpath from lnx import assets, exporter_opt, log, make_renderpath
from lnx.material import cycles, make as make_material, mat_batch from lnx.material import cycles, make as make_material, mat_batch
@ -45,6 +47,7 @@ if lnx.is_reload(__name__):
make_material = lnx.reload_module(make_material) make_material = lnx.reload_module(make_material)
mat_batch = lnx.reload_module(mat_batch) mat_batch = lnx.reload_module(mat_batch)
lnx.utils = lnx.reload_module(lnx.utils) lnx.utils = lnx.reload_module(lnx.utils)
linked_utils = lnx.reload_module(linked_utils)
lnx.profiler = lnx.reload_module(lnx.profiler) lnx.profiler = lnx.reload_module(lnx.profiler)
else: else:
lnx.enable_reload(__name__) lnx.enable_reload(__name__)
@ -67,9 +70,16 @@ class NodeType(Enum):
"""Returns the NodeType enum member belonging to the type of """Returns the NodeType enum member belonging to the type of
the given blender object.""" the given blender object."""
if bobject.type == "MESH": if bobject.type == "MESH":
if bobject.data.polygons: if bobject.data.polygons or bobject.data.edges or bobject.data.vertices:
return cls.MESH return cls.MESH
elif bobject.type in ('FONT', 'META'): elif bobject.type in ('FONT', 'META', 'CURVE'): # FIXME: curves with meshes shouldn't be used in modifiers for now.
if bobject.type == 'CURVE':
mesh = bobject.to_mesh()
if mesh is not None:
has_geometry = len(mesh.polygons) > 0
bobject.to_mesh_clear()
if not has_geometry:
return cls.EMPTY
return cls.MESH return cls.MESH
elif bobject.type == "LIGHT": elif bobject.type == "LIGHT":
return cls.LIGHT return cls.LIGHT
@ -148,6 +158,15 @@ class LeenkxExporter:
self.referenced_collections: List[bpy.types.Collection] = [] self.referenced_collections: List[bpy.types.Collection] = []
"""Collections referenced by collection instances""" """Collections referenced by collection instances"""
self.inlined_collections: set = set()
"""Linked collections inlined as children of their instance empty"""
self._collection_base_objects: dict = {}
"""collection -> list of deepcopy lnx objects (before transform composition) for cloning"""
self.inlined_empty_children: dict = {}
"""empty bobject -> list of exported child names, for fixing collection object_refs"""
self.has_spawning_camera = False self.has_spawning_camera = False
"""Whether there is at least one camera in the scene that spawns by default""" """Whether there is at least one camera in the scene that spawns by default"""
@ -296,8 +315,27 @@ class LeenkxExporter:
Modifiers that are not range-restricted are ignored in this Modifiers that are not range-restricted are ignored in this
calculation. calculation.
""" """
start = action.frame_range[0] frame_range = action.frame_range
end = action.frame_range[1] start = frame_range[0]
end = frame_range[1]
# Blender 4.0+ compatibility: Handle zero-length frame ranges
if start == end:
start = 1
end = 2
if action.fcurves:
all_keyframes = []
for fcurve in action.fcurves:
if fcurve.keyframe_points:
for keyframe in fcurve.keyframe_points:
all_keyframes.append(keyframe.co[0])
if all_keyframes:
start = min(all_keyframes)
end = max(all_keyframes)
if start == end:
end = start + 1
# Take FCurve modifiers into account if they have a restricted # Take FCurve modifiers into account if they have a restricted
# frame range # frame range
@ -331,22 +369,12 @@ class LeenkxExporter:
def export_object_transform(self, bobject: bpy.types.Object, o): def export_object_transform(self, bobject: bpy.types.Object, o):
wrd = bpy.data.worlds['Lnx'] wrd = bpy.data.worlds['Lnx']
# HACK: In Blender 4.2.x, each camera must be selected to ensure its matrix is correctly assigned # Use TransformEvaluator to handle linked object transform evaluation
if bpy.app.version >= (4, 2, 0) and bobject.type == 'CAMERA' and bobject.users_scene: with linked_utils.TransformEvaluator(bobject, self.scene, self.depsgraph) as evaluator:
current_scene = bpy.context.window.scene matrix_local = evaluator.matrix_local
bpy.context.window.scene = bobject.users_scene[0]
bpy.context.view_layer.update()
bobject.select_set(True)
bpy.context.view_layer.update()
bobject.select_set(False)
bpy.context.window.scene = current_scene
bpy.context.view_layer.update()
# Static transform # Static transform
o['transform'] = {'values': LeenkxExporter.write_matrix(bobject.matrix_local)} o['transform'] = {'values': LeenkxExporter.write_matrix(matrix_local)}
# Animated transform # Animated transform
if bobject.animation_data is not None and bobject.type != "ARMATURE": if bobject.animation_data is not None and bobject.type != "ARMATURE":
@ -445,9 +473,15 @@ class LeenkxExporter:
if btype is not NodeType.MESH and LeenkxExporter.option_mesh_only: if btype is not NodeType.MESH and LeenkxExporter.option_mesh_only:
return return
is_local_to_linked_scene = bobject.name in self.scene.objects and bobject.name not in self.scene.collection.children and self.scene.library
if bobject.type == 'CAMERA' and bobject.library:
struct_name = bobject.name + '_' + (os.path.basename(self.scene.library.filepath) if self.scene.library else self.scene.name)
else:
struct_name = linked_utils.asset_name(bobject)
self.bobject_array[bobject] = { self.bobject_array[bobject] = {
"objectType": btype, "objectType": btype,
"structName": lnx.utils.asset_name(bobject) "structName": struct_name
} }
if bobject.type == "ARMATURE": if bobject.type == "ARMATURE":
@ -491,7 +525,7 @@ class LeenkxExporter:
fcurve_list = self.collect_bone_animation(armature, bone.name) fcurve_list = self.collect_bone_animation(armature, bone.name)
if fcurve_list and pose_bone: if fcurve_list and pose_bone:
begin_frame, end_frame = int(action.frame_range[0]), int(action.frame_range[1]) begin_frame, end_frame = self.calculate_anim_frame_range(action)
out_track = {'target': "transform", 'frames': [], 'values': []} out_track = {'target': "transform", 'frames': [], 'values': []}
o['anim'] = {'tracks': [out_track]} o['anim'] = {'tracks': [out_track]}
@ -610,7 +644,7 @@ class LeenkxExporter:
return None return None
def write_bone_matrices(self, scene, action): def write_bone_matrices(self, scene, action):
begin_frame, end_frame = int(action.frame_range[0]), int(action.frame_range[1]) begin_frame, end_frame = self.calculate_anim_frame_range(action)
if len(self.bone_tracks) > 0: if len(self.bone_tracks) > 0:
hidden_states = {} hidden_states = {}
try: try:
@ -659,21 +693,31 @@ class LeenkxExporter:
# Skinning # Skinning
if lnx.utils.export_bone_data(bobject): if lnx.utils.export_bone_data(bobject):
variant_suffix = '_lnxskin' variant_suffix = '_lnxskin'
# Tilesheets # Tilesheets - check if object has tilesheet enabled
elif bobject.lnx_tilesheet != '': elif bobject.type == 'MESH' and len(bobject.material_slots) > 0:
if not bobject.lnx_use_custom_tilesheet_node: if bobject.lnx_tilesheet_enabled:
variant_suffix = '_lnxtile' variant_suffix = '_lnxtile'
elif lnx.utils.export_morph_targets(bobject): # For collection instances, check objects inside the instanced collection
elif bobject.instance_type == 'COLLECTION' and bobject.instance_collection is not None:
for cobj in bobject.instance_collection.all_objects:
if cobj.type == 'MESH' and cobj.lnx_tilesheet_enabled:
variant_suffix = '_lnxtile'
break
if variant_suffix == '' and lnx.utils.export_morph_targets(bobject):
variant_suffix = '_lnxskey' variant_suffix = '_lnxskey'
if variant_suffix == '': if variant_suffix == '':
continue continue
# For regular mesh objects, process their material slots
for slot in bobject.material_slots: for slot in bobject.material_slots:
if slot.material is None: if slot.material is None:
continue continue
# For linked materials, set the flag directly (can't create variants)
if slot.material.library is not None: if slot.material.library is not None:
slot.material.lnx_particle_flag = True if variant_suffix == '_lnxtile':
slot.material.lnx_tilesheet_flag = True
slot.material.lnx_cached = False
continue continue
if slot.material.name.endswith(variant_suffix): if slot.material.name.endswith(variant_suffix):
continue continue
@ -690,6 +734,23 @@ class LeenkxExporter:
matvars.append(mat) matvars.append(mat)
slot.material = mat slot.material = mat
# For collection instances, set tilesheet flag on materials of objects inside the collection
# ONLY for objects that have lnx_tilesheet_enabled set
if bobject.instance_type == 'COLLECTION' and bobject.instance_collection is not None:
for cobj in bobject.instance_collection.all_objects:
if cobj.type != 'MESH':
continue
# Only apply tilesheet flag to objects that have it enabled
if not cobj.lnx_tilesheet_enabled:
continue
for slot in cobj.material_slots:
if slot.material is None:
continue
# Set the flag and invalidate cache to force shader regeneration
if variant_suffix == '_lnxtile':
slot.material.lnx_tilesheet_flag = True
slot.material.lnx_cached = False
# Particle and non-particle objects can not share material # Particle and non-particle objects can not share material
particle_sys: bpy.types.ParticleSettings particle_sys: bpy.types.ParticleSettings
for particle_sys in bpy.data.particles: for particle_sys in bpy.data.particles:
@ -698,7 +759,10 @@ class LeenkxExporter:
continue continue
for slot in bobject.material_slots: for slot in bobject.material_slots:
if slot.material is None or slot.material.library is not None: if slot.material is None:
continue
if slot.material.library is not None:
slot.material.lnx_particle_flag = True
continue continue
if slot.material.name.endswith('_lnxpart'): if slot.material.name.endswith('_lnxpart'):
continue continue
@ -806,7 +870,7 @@ class LeenkxExporter:
# self.indentLevel -= 1 # self.indentLevel -= 1
# self.IndentWrite(B"}\n") # self.IndentWrite(B"}\n")
def export_object(self, bobject: bpy.types.Object, out_parent: Dict = None) -> None: def export_object(self, bobject: bpy.types.Object, out_parent: Dict = None, allow_inline: bool = True) -> None:
"""This function exports a single object in the scene and """This function exports a single object in the scene and
includes its name, object reference, material references (for includes its name, object reference, material references (for
meshes), and transform. meshes), and transform.
@ -815,6 +879,70 @@ class LeenkxExporter:
if not bobject.lnx_export: if not bobject.lnx_export:
return return
# Inline linked collections: skip the empty,
# export collection objects directly at this level
# If the empty has traits, preserve it as a wrapper (group_ref path)
# Only inline during Phase 2 (scene objects), not when called from export_collection
if allow_inline and bobject.instance_type == 'COLLECTION' and bobject.instance_collection is not None:
collection = bobject.instance_collection
empty_has_traits = hasattr(bobject, 'lnx_traitlist') and len(bobject.lnx_traitlist) > 0
if collection.library is not None and not empty_has_traits:
self.inlined_collections.add(collection)
empty_matrix = bobject.matrix_local
is_first = collection not in self._collection_base_objects
if is_first:
# First instance: do the full export (meshes, materials, etc.)
for cobj in collection.objects:
if cobj not in self.object_to_lnx_object_dict:
self.object_to_lnx_object_dict[cobj] = {'traits': []}
base_objects = []
for child in collection.objects:
if not child.lnx_export:
continue
if child.parent is not None and child.parent.name in collection.objects:
continue
if child.type == 'CAMERA':
asset_name = child.name + '_' + (os.path.basename(self.scene.library.filepath) if self.scene.library else self.scene.name)
self.output['camera_ref'] = asset_name
self.has_spawning_camera = True
self.process_bobject(child)
self.export_object(child, out_parent)
child_out = self.object_to_lnx_object_dict[child]
# Child may have been inlined (nested collection instance),
# in which case it has no 'name' — skip it from base_objects
if 'name' not in child_out:
continue
# Save a deep copy before composing (for cloning later)
base_objects.append(copy.deepcopy(child_out))
# Compose empty transform with child transform
if 'transform' in child_out:
v = child_out['transform']['values']
child_matrix = Matrix((v[0:4], v[4:8], v[8:12], v[12:16]))
composed = empty_matrix @ child_matrix
child_out['transform']['values'] = LeenkxExporter.write_matrix(composed)
self._collection_base_objects[collection] = base_objects
self.inlined_empty_children[bobject] = [obj['name'] for obj in base_objects]
else:
# Subsequent instances: clone from saved base objects
for base_obj in self._collection_base_objects[collection]:
instance_obj = copy.deepcopy(base_obj)
# Compose this instance's empty transform
if 'transform' in instance_obj:
v = instance_obj['transform']['values']
child_matrix = Matrix((v[0:4], v[4:8], v[8:12], v[12:16]))
composed = empty_matrix @ child_matrix
instance_obj['transform']['values'] = LeenkxExporter.write_matrix(composed)
if out_parent is None:
self.output['objects'].append(instance_obj)
else:
if 'children' not in out_parent:
out_parent['children'] = []
out_parent['children'].append(instance_obj)
self.inlined_empty_children[bobject] = [obj['name'] for obj in self._collection_base_objects[collection]]
return
bobject_ref = self.bobject_array.get(bobject) bobject_ref = self.bobject_array.get(bobject)
if bobject_ref is not None: if bobject_ref is not None:
object_type = bobject_ref["objectType"] object_type = bobject_ref["objectType"]
@ -852,13 +980,53 @@ class LeenkxExporter:
out_object['mobile'] = bobject.lnx_mobile out_object['mobile'] = bobject.lnx_mobile
lib = bobject.library
if lib is None and bobject.data:
lib = bobject.data.library
if lib is None and bobject.override_library:
lib = bobject.override_library.library
if lib is not None:
out_object['filename'] = lib.name
if bobject.instance_type == 'COLLECTION' and bobject.instance_collection is not None: if bobject.instance_type == 'COLLECTION' and bobject.instance_collection is not None:
out_object['group_ref'] = bobject.instance_collection.name out_object['group_ref'] = bobject.instance_collection.name
self.referenced_collections.append(bobject.instance_collection) self.referenced_collections.append(bobject.instance_collection)
if bobject.lnx_tilesheet != '': # Export tilesheet data if enabled on this object
out_object['tilesheet_ref'] = bobject.lnx_tilesheet if bobject.lnx_tilesheet_enabled:
out_object['tilesheet_action_ref'] = bobject.lnx_tilesheet_action out_object['tilesheet'] = {
'start_action': bobject.lnx_tilesheet_default_action,
'flipx': bobject.lnx_tilesheet_flipx,
'flipy': bobject.lnx_tilesheet_flipy,
'actions': []
}
for action in bobject.lnx_tilesheet_actionlist:
action_data = {
'name': action.name,
'start': action.start_prop,
'end': action.end_prop,
'loop': action.loop_prop,
'tilesx': action.tilesx_prop,
'tilesy': action.tilesy_prop,
'framerate': action.framerate_prop
}
# Mesh swap reference (for later implementation)
if action.mesh_prop != '':
# Look up the actual mesh to get proper name with library suffix if linked
mesh_data = bpy.data.meshes.get(action.mesh_prop)
if mesh_data is not None:
action_data['mesh'] = lnx.utils.safestr(lnx.utils.asset_name(mesh_data))
else:
action_data['mesh'] = lnx.utils.safestr(action.mesh_prop)
# Export events if any
if len(action.events) > 0:
action_data['events'] = []
for evt in action.events:
action_data['events'].append({
'name': evt.name,
'frame': evt.frame_prop
})
out_object['tilesheet']['actions'].append(action_data)
if len(bobject.vertex_groups) > 0 and bobject.data is not None and len(bobject.data.vertices) > 0: if len(bobject.vertex_groups) > 0 and bobject.data is not None and len(bobject.data.vertices) > 0:
out_object['vertex_groups'] = [] out_object['vertex_groups'] = []
@ -905,6 +1073,7 @@ class LeenkxExporter:
# Check if the property is a collection (array type). # Check if the property is a collection (array type).
if proplist_item.type_prop == 'array': if proplist_item.type_prop == 'array':
# Convert the collection to a list. # Convert the collection to a list.
array_type = proplist_item.array_item_type array_type = proplist_item.array_item_type
collection_value = getattr(proplist_item, 'array_prop') collection_value = getattr(proplist_item, 'array_prop')
property_name = array_type + '_prop' property_name = array_type + '_prop'
@ -923,7 +1092,7 @@ class LeenkxExporter:
# Export the object reference and material references # Export the object reference and material references
objref = bobject.data objref = bobject.data
if objref is not None: if objref is not None:
objname = lnx.utils.asset_name(objref) objname = linked_utils.asset_name(objref)
# LOD # LOD
if bobject.type == 'MESH' and hasattr(objref, 'lnx_lodlist') and len(objref.lnx_lodlist) > 0: if bobject.type == 'MESH' and hasattr(objref, 'lnx_lodlist') and len(objref.lnx_lodlist) > 0:
@ -973,8 +1142,7 @@ class LeenkxExporter:
out_object['particle_refs'] = [] out_object['particle_refs'] = []
out_object['render_emitter'] = bobject.show_instancer_for_render out_object['render_emitter'] = bobject.show_instancer_for_render
for i in range(num_psys): for i in range(num_psys):
for obj in bpy.data.objects: for mod in bobject.modifiers:
for mod in obj.modifiers:
if mod.type == 'PARTICLE_SYSTEM': if mod.type == 'PARTICLE_SYSTEM':
if mod.particle_system.name == bobject.particle_systems[i].name: if mod.particle_system.name == bobject.particle_systems[i].name:
if mod.show_render: if mod.show_render:
@ -1145,9 +1313,11 @@ class LeenkxExporter:
_bake_hidden[_obj] = False _bake_hidden[_obj] = False
_obj.hide_viewport = True _obj.hide_viewport = True
start, end = self.calculate_anim_frame_range(action)
bake_result = bpy.ops.nla.bake( bake_result = bpy.ops.nla.bake(
frame_start=int(action.frame_range[0]), frame_start=start,
frame_end=int(action.frame_range[1]), frame_end=end,
step=1, step=1,
only_selected=False, only_selected=False,
visual_keying=True visual_keying=True
@ -1210,7 +1380,7 @@ class LeenkxExporter:
self.post_export_object(bobject, out_object, object_type) self.post_export_object(bobject, out_object, object_type)
if not hasattr(out_object, 'children') and len(bobject.children) > 0: if 'children' not in out_object and len(bobject.children) > 0:
out_object['children'] = [] out_object['children'] = []
if bobject.lnx_instanced == 'Off': if bobject.lnx_instanced == 'Off':
@ -1650,6 +1820,7 @@ class LeenkxExporter:
maxdim_uvlayer = lay0 maxdim_uvlayer = lay0
_uv0_raw = np.empty(len(lay0.data) * 2, dtype='<f4') _uv0_raw = np.empty(len(lay0.data) * 2, dtype='<f4')
lay0.data.foreach_get('uv', _uv0_raw) lay0.data.foreach_get('uv', _uv0_raw)
_uv0_raw[1::2] = np.abs(1.0 - _uv0_raw[1::2])
_uv0_absmax = float(np.abs(_uv0_raw).max()) if len(_uv0_raw) > 0 else 0.0 _uv0_absmax = float(np.abs(_uv0_raw).max()) if len(_uv0_raw) > 0 else 0.0
if _uv0_absmax > maxdim: if _uv0_absmax > maxdim:
maxdim = _uv0_absmax maxdim = _uv0_absmax
@ -1657,6 +1828,7 @@ class LeenkxExporter:
lay1 = uv_layers[t1map] lay1 = uv_layers[t1map]
_uv1_raw = np.empty(len(lay1.data) * 2, dtype='<f4') _uv1_raw = np.empty(len(lay1.data) * 2, dtype='<f4')
lay1.data.foreach_get('uv', _uv1_raw) lay1.data.foreach_get('uv', _uv1_raw)
_uv1_raw[1::2] = np.abs(1.0 - _uv1_raw[1::2])
_uv1_absmax = float(np.abs(_uv1_raw).max()) if len(_uv1_raw) > 0 else 0.0 _uv1_absmax = float(np.abs(_uv1_raw).max()) if len(_uv1_raw) > 0 else 0.0
if _uv1_absmax > maxdim: if _uv1_absmax > maxdim:
maxdim = _uv1_absmax maxdim = _uv1_absmax
@ -1666,6 +1838,7 @@ class LeenkxExporter:
lay2 = uv_layers[morph_uv_index] lay2 = uv_layers[morph_uv_index]
_uv2_raw = np.empty(len(lay2.data) * 2, dtype='<f4') _uv2_raw = np.empty(len(lay2.data) * 2, dtype='<f4')
lay2.data.foreach_get('uv', _uv2_raw) lay2.data.foreach_get('uv', _uv2_raw)
_uv2_raw[1::2] = np.abs(1.0 - _uv2_raw[1::2])
_uv2_absmax = float(np.abs(_uv2_raw).max()) if len(_uv2_raw) > 0 else 0.0 _uv2_absmax = float(np.abs(_uv2_raw).max()) if len(_uv2_raw) > 0 else 0.0
if _uv2_absmax > maxdim: if _uv2_absmax > maxdim:
maxdim = _uv2_absmax maxdim = _uv2_absmax
@ -1919,16 +2092,27 @@ class LeenkxExporter:
armature = bobject.find_armature() armature = bobject.find_armature()
apply_modifiers = not armature apply_modifiers = not armature
bobject_eval = bobject.evaluated_get(self.depsgraph) if apply_modifiers else bobject if apply_modifiers:
export_mesh = bobject_eval.to_mesh() # HACK: For linked objects with duplicate names, we need to force evaluation
# by temporarily adding the object to the current scene's collection
with linked_utils.evaluated_mesh(bobject, self.scene, self.depsgraph, apply_modifiers=True) as (bobject_eval, _):
export_mesh = bobject_eval.to_mesh(preserve_all_data_layers=True, depsgraph=self.depsgraph)
else:
bobject_eval = bobject
export_mesh = bobject_eval.to_mesh(preserve_all_data_layers=True, depsgraph=self.depsgraph)
# Export shape keys here # Export shape keys here
if shape_keys: if shape_keys:
self.export_shape_keys(bobject, export_mesh, out_mesh) self.export_shape_keys(bobject, export_mesh, out_mesh)
# Update dependancy after new UV layer was added # Update dependency after new UV layer was added
self.depsgraph.update() self.depsgraph.update()
bobject_eval = bobject.evaluated_get(self.depsgraph) if apply_modifiers else bobject if apply_modifiers:
export_mesh = bobject_eval.to_mesh() # Force individual evaluation again after shape key changes
with linked_utils.evaluated_mesh(bobject, self.scene, self.depsgraph, apply_modifiers=True) as (bobject_eval, _):
export_mesh = bobject_eval.to_mesh(preserve_all_data_layers=True, depsgraph=self.depsgraph)
else:
bobject_eval = bobject
export_mesh = bobject_eval.to_mesh(preserve_all_data_layers=True, depsgraph=self.depsgraph)
if export_mesh is None: if export_mesh is None:
log.warn(oid + ' was not exported') log.warn(oid + ' was not exported')
@ -1980,7 +2164,30 @@ class LeenkxExporter:
"""Exports a single light object.""" """Exports a single light object."""
rpdat = lnx.utils.get_rp() rpdat = lnx.utils.get_rp()
light_ref = object_ref[0] light_ref = object_ref[0]
light_objects = object_ref[1]["objectTable"]
light_object = light_objects[0] if len(light_objects) > 0 else None
objtype = light_ref.type objtype = light_ref.type
color = [light_ref.color[0], light_ref.color[1], light_ref.color[2]]
if light_ref.use_temperature:
temperature_color = light_ref.temperature_color
color[0] *= temperature_color[0]
color[1] *= temperature_color[1]
color[2] *= temperature_color[2]
strength = light_ref.energy * math.pow(2.0, light_ref.exposure)
if not light_ref.normalize:
area = 0.0
try:
if light_object is not None:
area = light_ref.area(matrix_world=light_object.matrix_world)
else:
area = light_ref.area()
except TypeError:
area = light_ref.area()
if area > 0.0:
strength *= area
out_light = { out_light = {
'name': object_ref[1]["structName"], 'name': object_ref[1]["structName"],
'type': objtype.lower(), 'type': objtype.lower(),
@ -1988,8 +2195,8 @@ class LeenkxExporter:
'near_plane': light_ref.lnx_clip_start, 'near_plane': light_ref.lnx_clip_start,
'far_plane': light_ref.lnx_clip_end, 'far_plane': light_ref.lnx_clip_end,
'fov': light_ref.lnx_fov, 'fov': light_ref.lnx_fov,
'color': [light_ref.color[0], light_ref.color[1], light_ref.color[2]], 'color': color,
'strength': light_ref.energy, 'strength': strength,
'shadows_bias': light_ref.lnx_shadows_bias * 0.0001 'shadows_bias': light_ref.lnx_shadows_bias * 0.0001
} }
if rpdat.rp_shadows: if rpdat.rp_shadows:
@ -2001,25 +2208,37 @@ class LeenkxExporter:
out_light['shadowmap_size'] = 0 out_light['shadowmap_size'] = 0
if objtype == 'SUN': if objtype == 'SUN':
out_light['strength'] *= 0.325
# Scale bias for ortho light matrix # Scale bias for ortho light matrix
out_light['shadows_bias'] *= 20.0 out_light['shadows_bias'] *= 20.0
if out_light['shadowmap_size'] > 1024: if out_light['shadowmap_size'] > 1024:
# Less bias for bigger maps # Less bias for bigger maps
out_light['shadows_bias'] *= 1 / (out_light['shadowmap_size'] / 1024) out_light['shadows_bias'] *= 1 / (out_light['shadowmap_size'] / 1024)
elif objtype == 'POINT': elif objtype == 'POINT':
out_light['strength'] *= 0.01 out_light['strength'] *= 1.0 / (4.0 * math.pi)
out_light['fov'] = 1.5708 # pi/2 out_light['fov'] = 1.5708 # pi/2
out_light['shadowmap_cube'] = True out_light['shadowmap_cube'] = True
if light_ref.shadow_soft_size > 0.1: if light_ref.shadow_soft_size > 0.0:
out_light['light_size'] = light_ref.shadow_soft_size * 10 out_light['light_size'] = light_ref.shadow_soft_size * 10
elif objtype == 'SPOT': elif objtype == 'SPOT':
out_light['strength'] *= 0.01 out_light['strength'] *= 1.0 / (4.0 * math.pi)
out_light['spot_size'] = math.cos(light_ref.spot_size / 2) half_angle = light_ref.spot_size * 0.5
# Cycles defaults to 0.15 outer_cos = math.cos(half_angle)
out_light['spot_blend'] = light_ref.spot_blend / 10 blend = max(0.0, min(1.0, light_ref.spot_blend))
inner_angle = math.atan(math.tan(half_angle) * (1.0 - blend))
out_light['spot_size'] = outer_cos
out_light['spot_blend'] = max(0.0001, math.cos(inner_angle) - outer_cos)
if light_ref.shadow_soft_size > 0.0:
out_light['light_size'] = light_ref.shadow_soft_size * 10
elif objtype == 'AREA': elif objtype == 'AREA':
out_light['strength'] *= 0.01 light_area = light_ref.size * light_ref.size_y
if light_ref.shape in ('DISK', 'ELLIPSE'):
light_area *= math.pi / 4.0
if light_area > 0.0:
out_light['strength'] *= 1.0 / (light_area * math.pi)
else:
out_light['strength'] *= 1.0 / (math.pi)
out_light['size'] = light_ref.size out_light['size'] = light_ref.size
out_light['size_y'] = light_ref.size_y out_light['size_y'] = light_ref.size_y
@ -2052,10 +2271,21 @@ class LeenkxExporter:
if not bobject.lnx_export: if not bobject.lnx_export:
continue continue
# If this object was an inlined collection instance,
# reference its exported children instead of the removed empty
if bobject in self.inlined_empty_children:
for child_name in self.inlined_empty_children[bobject]:
out_collection['object_refs'].append(child_name)
continue
# Only add unparented objects or objects with their parent # Only add unparented objects or objects with their parent
# outside the collection, then instantiate the full object # outside the collection, then instantiate the full object
# child tree if the collection gets spawned as a whole # child tree if the collection gets spawned as a whole
if bobject.parent is None or bobject.parent.name not in collection.objects: if bobject.parent is None or bobject.parent.name not in collection.objects:
is_local_to_linked_scene = bobject.name in self.scene.objects and bobject.name not in self.scene.collection.children and self.scene.library
if bobject.type == 'CAMERA':
asset_name = bobject.name + '_' + (os.path.basename(self.scene.library.filepath) if self.scene.library else self.scene.name)
else:
asset_name = lnx.utils.asset_name(bobject) asset_name = lnx.utils.asset_name(bobject)
if collection.library and not collection.name in self.scene.collection.children: if collection.library and not collection.name in self.scene.collection.children:
@ -2074,7 +2304,11 @@ class LeenkxExporter:
continue continue
self.process_bobject(bobject) self.process_bobject(bobject)
self.export_object(bobject) self.export_object(bobject, allow_inline=False)
if bobject.type == 'CAMERA':
self.output['camera_ref'] = asset_name
self.has_spawning_camera = True
out_collection['object_refs'].append(asset_name) out_collection['object_refs'].append(asset_name)
@ -2275,7 +2509,7 @@ class LeenkxExporter:
material.signature = signature material.signature = signature
o = {} o = {}
o['name'] = lnx.utils.asset_name(material) o['name'] = lnx.linked_utils.asset_name(material)
if material.lnx_skip_context != '': if material.lnx_skip_context != '':
o['skip_context'] = material.lnx_skip_context o['skip_context'] = material.lnx_skip_context
@ -2385,15 +2619,18 @@ class LeenkxExporter:
if len(self.particle_system_array) > 0: if len(self.particle_system_array) > 0:
self.output['particle_datas'] = [] self.output['particle_datas'] = []
for particleRef in self.particle_system_array.items(): for particleRef in self.particle_system_array.items():
padd = False;
padd = False
for obj in bpy.data.objects: for obj in bpy.data.objects:
for mod in obj.modifiers: for mod in obj.modifiers:
if mod.type == 'PARTICLE_SYSTEM': if mod.type == 'PARTICLE_SYSTEM':
if mod.particle_system.settings.name == particleRef[1]["structName"]: if mod.particle_system.settings.name == particleRef[1]["structName"]:
if mod.show_render: if mod.show_render:
padd = True padd = True
if not padd: if not padd:
continue; continue
psettings = particleRef[0] psettings = particleRef[0]
if psettings is None: if psettings is None:
@ -2408,12 +2645,41 @@ class LeenkxExporter:
elif psettings.emit_from == 'VOLUME': elif psettings.emit_from == 'VOLUME':
emit_from = 2 emit_from = 2
if psettings.lnx_rotation_mode == 'NONE':
rotation_mode = 0
elif psettings.lnx_rotation_mode == 'NOR':
rotation_mode = 1
elif psettings.lnx_rotation_mode == 'NOR_TAN':
rotation_mode = 2
elif psettings.lnx_rotation_mode == 'VEL':
rotation_mode = 3
elif psettings.lnx_rotation_mode == 'GLOB_X':
rotation_mode = 4
elif psettings.lnx_rotation_mode == 'GLOB_Y':
rotation_mode = 5
elif psettings.lnx_rotation_mode == 'GLOB_Z':
rotation_mode = 6
elif psettings.lnx_rotation_mode == 'OB_X':
rotation_mode = 7
elif psettings.lnx_rotation_mode == 'OB_Y':
rotation_mode = 8
elif psettings.lnx_rotation_mode == 'OB_Z':
rotation_mode = 9
# For CPU particles
texture_slots = {}
for key, slot in psettings.texture_slots.items():
slot_data = self.extract_props(slot)
texture_slots[key] = slot_data
out_particlesys = { out_particlesys = {
'name': particleRef[1]["structName"], 'name': particleRef[1]["structName"],
'type': 0 if psettings.type == 'EMITTER' else 1, # HAIR 'type': 0 if psettings.type == 'EMITTER' else 1, # HAIR
'auto_start': psettings.lnx_auto_start, 'auto_start': psettings.lnx_auto_start,
'dynamic_emitter': psettings.lnx_dynamic_emitter, 'dynamic_emitter': psettings.lnx_dynamic_emitter,
'is_unique': psettings.lnx_is_unique, 'is_unique': psettings.lnx_is_unique,
'local_coords': psettings.lnx_local_coords,
'loop': psettings.lnx_loop, 'loop': psettings.lnx_loop,
# Emission # Emission
'count': int(psettings.count * psettings.lnx_count_mult), 'count': int(psettings.count * psettings.lnx_count_mult),
@ -2433,6 +2699,13 @@ class LeenkxExporter:
), ),
# 'object_factor': psettings.object_factor, # 'object_factor': psettings.object_factor,
'factor_random': psettings.factor_random, 'factor_random': psettings.factor_random,
# Rotation
'use_rotations': psettings.lnx_use_rotations,
'rotation_mode': rotation_mode,
'rotation_factor_random': psettings.lnx_rotation_factor_random,
'phase_factor': psettings.lnx_phase_factor,
'phase_factor_random': psettings.lnx_phase_factor_random,
'use_dynamic_rotation': psettings.lnx_use_dynamic_rotation,
# Physics # Physics
'physics_type': 1 if psettings.physics_type == 'NEWTON' else 0, 'physics_type': 1 if psettings.physics_type == 'NEWTON' else 0,
'particle_size': psettings.particle_size, 'particle_size': psettings.particle_size,
@ -2441,7 +2714,10 @@ class LeenkxExporter:
# Render # Render
'instance_object': lnx.utils.asset_name(psettings.instance_object), 'instance_object': lnx.utils.asset_name(psettings.instance_object),
# Field weights # Field weights
'weight_gravity': psettings.effector_weights.gravity 'weight_gravity': psettings.effector_weights.gravity,
'weight_texture': psettings.effector_weights.texture,
# Textures
'texture_slots': texture_slots # For CPU particles
} }
if psettings.instance_object not in self.object_to_lnx_object_dict: if psettings.instance_object not in self.object_to_lnx_object_dict:
@ -2453,25 +2729,47 @@ class LeenkxExporter:
self.output['particle_datas'].append(out_particlesys) self.output['particle_datas'].append(out_particlesys)
def export_tilesheets(self): # For CPU particles
wrd = bpy.data.worlds['Lnx'] def extract_props(self, bpy_struct, depth=0, max_depth=2):
if len(wrd.lnx_tilesheetlist) > 0: result = {}
self.output['tilesheet_datas'] = [] for prop in bpy_struct.bl_rna.properties:
for ts in wrd.lnx_tilesheetlist: name = prop.identifier
o = {} if name == "rna_type":
o['name'] = ts.name continue
o['tilesx'] = ts.tilesx_prop try:
o['tilesy'] = ts.tilesy_prop value = getattr(bpy_struct, name)
o['framerate'] = ts.framerate_prop
o['actions'] = [] if name == "color_ramp" and hasattr(value, "elements"):
for tsa in ts.lnx_tilesheetactionlist: result[name] = {
ao = {} "elements": [
ao['name'] = tsa.name {
ao['start'] = tsa.start_prop "position": el.position,
ao['end'] = tsa.end_prop "color": {
ao['loop'] = tsa.loop_prop "r": el.color[0],
o['actions'].append(ao) "g": el.color[1],
self.output['tilesheet_datas'].append(o) "b": el.color[2],
"a": el.color[3]
}
}
for el in value.elements
],
"interpolation": value.interpolation,
"hue_interpolation": value.hue_interpolation,
"color_mode": value.color_mode
}
elif isinstance(value, (int, float, bool, str)):
result[name] = value
elif isinstance(value, (tuple, list)):
result[name] = list(value)
elif hasattr(value, "bl_rna") and depth < max_depth:
result[name] = self.extract_props(value, depth + 1, max_depth)
else:
result[name] = str(value)
except Exception as e:
result[name] = f"<unreadable: {e}>"
return result
def export_world(self): def export_world(self):
"""Exports the world of the current scene.""" """Exports the world of the current scene."""
@ -2562,7 +2860,7 @@ class LeenkxExporter:
self.process_skinned_meshes() self.process_skinned_meshes()
self.output['name'] = lnx.utils.safestr(self.scene.name) self.output['name'] = lnx.utils.safestr(self.scene.name + "_" + os.path.basename(self.scene.library.filepath).replace(".blend", "") if self.scene.library else self.scene.name)
if self.filepath.endswith('.lz4'): if self.filepath.endswith('.lz4'):
self.output['name'] += '.lz4' self.output['name'] += '.lz4'
elif not bpy.data.worlds['Lnx'].lnx_minimize: elif not bpy.data.worlds['Lnx'].lnx_minimize:
@ -2627,12 +2925,13 @@ class LeenkxExporter:
if collection.name.startswith(('RigidBodyWorld', 'Trait|')): if collection.name.startswith(('RigidBodyWorld', 'Trait|')):
continue continue
if self.scene.user_of_id(collection) or collection in self.referenced_collections: if self.scene.user_of_id(collection) or collection.library and not self.scene.library or collection in self.referenced_collections:
if collection not in self.inlined_collections:
self.export_collection(collection) self.export_collection(collection)
if not LeenkxExporter.option_mesh_only: if not LeenkxExporter.option_mesh_only:
if self.scene.camera is not None: if self.scene.camera is not None:
self.output['camera_ref'] = lnx.utils.asset_name(self.scene.camera) if self.scene.library else self.scene.camera.name self.output['camera_ref'] = lnx.utils.asset_name(self.scene.camera) if self.scene.camera.library else self.scene.camera.name + "_" + self.scene.name if self.scene.library else self.scene.camera.name
else: else:
if self.scene.name == lnx.utils.get_project_scene_name(): if self.scene.name == lnx.utils.get_project_scene_name():
log.warn(f'Scene "{self.scene.name}" is missing a camera') log.warn(f'Scene "{self.scene.name}" is missing a camera')
@ -2653,7 +2952,6 @@ class LeenkxExporter:
self.export_particle_systems() self.export_particle_systems()
self.output['world_datas'] = [] self.output['world_datas'] = []
self.export_world() self.export_world()
self.export_tilesheets()
if self.scene.world is not None: if self.scene.world is not None:
self.output['world_ref'] = lnx.utils.safestr(lnx.utils.asset_name(self.scene.world) if self.scene.world.library else self.scene.world.name) self.output['world_ref'] = lnx.utils.safestr(lnx.utils.asset_name(self.scene.world) if self.scene.world.library else self.scene.world.name)
@ -2852,6 +3150,13 @@ class LeenkxExporter:
# Rigid body trait # Rigid body trait
if bobject.rigid_body is not None and phys_enabled: if bobject.rigid_body is not None and phys_enabled:
# Skip children of compound parents - their shapes are baked into the parent
is_compound_child = (bobject.parent is not None and
bobject.parent.rigid_body is not None and
bobject.parent.rigid_body.collision_shape == 'COMPOUND')
if is_compound_child:
pass # Don't export RigidBody trait for compound children
else:
LeenkxExporter.export_physics = True LeenkxExporter.export_physics = True
rb = bobject.rigid_body rb = bobject.rigid_body
shape = 0 # BOX shape = 0 # BOX
@ -2868,6 +3173,8 @@ class LeenkxExporter:
shape = 5 shape = 5
elif rb.collision_shape == 'CAPSULE': elif rb.collision_shape == 'CAPSULE':
shape = 6 shape = 6
elif rb.collision_shape == 'COMPOUND':
shape = 8
body_mass = rb.mass body_mass = rb.mass
is_static = self.rigid_body_static(rb) is_static = self.rigid_body_static(rb)
@ -2923,7 +3230,7 @@ class LeenkxExporter:
body_params['angularFriction'] = bobject.lnx_rb_angular_friction body_params['angularFriction'] = bobject.lnx_rb_angular_friction
body_params['collisionMargin'] = col_margin body_params['collisionMargin'] = col_margin
body_params['linearDeactivationThreshold'] = deact_lv body_params['linearDeactivationThreshold'] = deact_lv
body_params['angularDeactivationThrshold'] = deact_av body_params['angularDeactivationThreshold'] = deact_av
body_params['deactivationTime'] = deact_time body_params['deactivationTime'] = deact_time
# New velocity limit properties # New velocity limit properties
body_params['linearVelocityMin'] = bobject.lnx_rb_linear_velocity_min body_params['linearVelocityMin'] = bobject.lnx_rb_linear_velocity_min
@ -2937,6 +3244,44 @@ class LeenkxExporter:
body_params['lockRotationX'] = bobject.lnx_rb_lock_rotation_x body_params['lockRotationX'] = bobject.lnx_rb_lock_rotation_x
body_params['lockRotationY'] = bobject.lnx_rb_lock_rotation_y body_params['lockRotationY'] = bobject.lnx_rb_lock_rotation_y
body_params['lockRotationZ'] = bobject.lnx_rb_lock_rotation_z body_params['lockRotationZ'] = bobject.lnx_rb_lock_rotation_z
# Collect compound children shapes if this is a compound parent
if rb.collision_shape == 'COMPOUND':
compound_children = []
for child in bobject.children:
if child.rigid_body is not None:
child_rb = child.rigid_body
# Map child collision shape to int
child_shape = 0 # BOX default
if child_rb.collision_shape == 'SPHERE':
child_shape = 1
elif child_rb.collision_shape == 'CONVEX_HULL':
child_shape = 2
elif child_rb.collision_shape == 'MESH':
child_shape = 3
elif child_rb.collision_shape == 'CONE':
child_shape = 4
elif child_rb.collision_shape == 'CYLINDER':
child_shape = 5
elif child_rb.collision_shape == 'CAPSULE':
child_shape = 6
# Get child's local transform relative to parent
local_matrix = bobject.matrix_world.inverted() @ child.matrix_world
loc = local_matrix.to_translation()
rot = local_matrix.to_quaternion()
# Get child dimensions
child_dim = child.dimensions
compound_children.append({
'shape': child_shape,
'posX': loc.x, 'posY': loc.y, 'posZ': loc.z,
'rotX': rot.x, 'rotY': rot.y, 'rotZ': rot.z, 'rotW': rot.w,
'dimX': child_dim.x, 'dimY': child_dim.y, 'dimZ': child_dim.z
})
body_params['compoundChildren'] = compound_children
body_flags = {} body_flags = {}
body_flags['animated'] = rb.kinematic body_flags['animated'] = rb.kinematic
body_flags['trigger'] = bobject.lnx_rb_trigger body_flags['trigger'] = bobject.lnx_rb_trigger
@ -3106,7 +3451,7 @@ class LeenkxExporter:
apply_modifiers = not armature apply_modifiers = not armature
bobject_eval = bobject.evaluated_get(self.depsgraph) if apply_modifiers else bobject bobject_eval = bobject.evaluated_get(self.depsgraph) if apply_modifiers else bobject
export_mesh = bobject_eval.to_mesh() export_mesh = bobject_eval.to_mesh(preserve_all_data_layers=True, depsgraph=self.depsgraph)
with open(nav_filepath, 'w') as f: with open(nav_filepath, 'w') as f:
for v in export_mesh.vertices: for v in export_mesh.vertices:
@ -3139,8 +3484,6 @@ class LeenkxExporter:
if trait_prop.type.endswith("Object"): if trait_prop.type.endswith("Object"):
value = lnx.utils.asset_name(trait_prop.value_object) value = lnx.utils.asset_name(trait_prop.value_object)
elif trait_prop.type == "TSceneFormat":
value = lnx.utils.asset_name(trait_prop.value_scene)
else: else:
value = trait_prop.get_value() value = trait_prop.get_value()
@ -3466,7 +3809,7 @@ class LeenkxExporter:
strength = world.lnx_envtex_strength strength = world.lnx_envtex_strength
mobile_mat = rpdat.lnx_material_model in ('Mobile', 'Solid') mobile_mat = rpdat.lnx_material_model in ('Mobile', 'Solid')
if mobile_mat: if mobile_mat or '_EnvCol' in world.world_defs:
lnx_radiance = False lnx_radiance = False
out_probe = {'name': lnx.utils.asset_name(world) if world.library else world.name} out_probe = {'name': lnx.utils.asset_name(world) if world.library else world.name}

View File

@ -133,7 +133,7 @@ def export_mesh_data(self, export_mesh: bpy.types.Mesh, bobject: bpy.types.Objec
# Shape keys UV are exported separately, so reduce UV count by 1 # Shape keys UV are exported separately, so reduce UV count by 1
num_uv_layers -= 1 num_uv_layers -= 1
morph_uv_index = self.get_morph_uv_index(bobject.data) morph_uv_index = self.get_morph_uv_index(bobject.data)
has_tex = self.get_export_uvs(export_mesh) or num_uv_layers > 0 # TODO FIXME: this should use an `and` instead of `or`. Workaround to completely ignore if the mesh has the `export_uvs` flag. Only checking the `uv_layers` to bypass issues with materials in linked objects. has_tex = self.get_export_uvs(export_mesh) or num_uv_layers > 0 # FIXME: this should use an `and` instead of `or`. Workaround to completely ignore if the mesh has the `export_uvs` flag. Only checking the `uv_layers` to bypass issues with materials in linked objects.
if self.has_baked_material(bobject, export_mesh.materials): if self.has_baked_material(bobject, export_mesh.materials):
has_tex = True has_tex = True
has_tex1 = has_tex and num_uv_layers > 1 has_tex1 = has_tex and num_uv_layers > 1
@ -177,16 +177,16 @@ def export_mesh_data(self, export_mesh: bpy.types.Mesh, bobject: bpy.types.Objec
for v in lay0.data: for v in lay0.data:
if abs(v.uv[0]) > maxdim: if abs(v.uv[0]) > maxdim:
maxdim = abs(v.uv[0]) maxdim = abs(v.uv[0])
if abs(v.uv[1]) > maxdim: if abs(1.0 - v.uv[1]) > maxdim:
maxdim = abs(v.uv[1]) maxdim = abs(1.0 - v.uv[1])
if has_tex1: if has_tex1:
lay1 = uv_layers[t1map] lay1 = uv_layers[t1map]
for v in lay1.data: for v in lay1.data:
if abs(v.uv[0]) > maxdim: if abs(v.uv[0]) > maxdim:
maxdim = abs(v.uv[0]) maxdim = abs(v.uv[0])
maxdim_uvlayer = lay1 maxdim_uvlayer = lay1
if abs(v.uv[1]) > maxdim: if abs(1.0 - v.uv[1]) > maxdim:
maxdim = abs(v.uv[1]) maxdim = abs(1.0 - v.uv[1])
maxdim_uvlayer = lay1 maxdim_uvlayer = lay1
if has_morph_target: if has_morph_target:
morph_data = np.empty(num_verts * 2, dtype='<f4') morph_data = np.empty(num_verts * 2, dtype='<f4')
@ -195,8 +195,8 @@ def export_mesh_data(self, export_mesh: bpy.types.Mesh, bobject: bpy.types.Objec
if abs(v.uv[0]) > maxdim: if abs(v.uv[0]) > maxdim:
maxdim = abs(v.uv[0]) maxdim = abs(v.uv[0])
maxdim_uvlayer = lay2 maxdim_uvlayer = lay2
if abs(v.uv[1]) > maxdim: if abs(1.0 - v.uv[1]) > maxdim:
maxdim = abs(v.uv[1]) maxdim = abs(1.0 - v.uv[1])
maxdim_uvlayer = lay2 maxdim_uvlayer = lay2
if maxdim > 1: if maxdim > 1:
o['scale_tex'] = maxdim o['scale_tex'] = maxdim

View File

@ -344,7 +344,7 @@ def apply_materials(load_atlas=0):
else: else:
mat.node_tree.links.new(lightmapNode.outputs[0], mixNode.inputs[6]) #Connect lightmap node to mixnode mat.node_tree.links.new(lightmapNode.outputs[0], mixNode.inputs[6]) #Connect lightmap node to mixnode
mat.node_tree.links.new(baseColorNode.outputs[0], mixNode.inputs[7]) #Connect basecolor to pbr node mat.node_tree.links.new(baseColorNode.outputs[0], mixNode.inputs[7]) #Connect basecolor to pbr node
mat.node_tree.links.new(mixNode.outputs[0], mainNode.inputs[2]) #Connect mixnode to pbr node mat.node_tree.links.new(mixNode.outputs[2], mainNode.inputs[0]) #Connect mixnode to pbr node
if not scene.TLM_EngineProperties.tlm_target == "vertex": if not scene.TLM_EngineProperties.tlm_target == "vertex":
mat.node_tree.links.new(UVLightmap.outputs[0], lightmapNode.inputs[0]) #Connect uvnode to lightmapnode mat.node_tree.links.new(UVLightmap.outputs[0], lightmapNode.inputs[0]) #Connect uvnode to lightmapnode

View File

@ -811,7 +811,7 @@ def set_settings():
print(bpy.app.version) print(bpy.app.version)
if bpy.app.version[0] == 3 or byp.app.version[0] == 4: if bpy.app.version[0] == 3 or bpy.app.version[0] == 4:
if cycles.device == "GPU": if cycles.device == "GPU":
scene.cycles.tile_size = 256 scene.cycles.tile_size = 256
else: else:

View File

@ -0,0 +1,144 @@
"""Utilities for handling linked blend files in Leenkx exports."""
from contextlib import contextmanager
from pathlib import Path
from typing import Dict, Optional
import bpy
import lnx
if lnx.is_reload(__name__):
pass
else:
lnx.enable_reload(__name__)
def is_linked(bdata) -> bool:
"""Check if a data block is linked from an external library."""
if bdata is None:
return False
return bdata.library is not None
def get_library_name(bdata) -> Optional[str]:
"""Get the library filename for a linked data block."""
if bdata is None or bdata.library is None:
return None
return bdata.library.name
def get_library_path(bdata) -> Optional[Path]:
"""Get the absolute path to the library .blend file."""
if bdata is None or bdata.library is None:
return None
return Path(bpy.path.abspath(bdata.library.filepath))
def asset_name(bdata) -> Optional[str]:
"""Get qualified asset name with library suffix for linked data."""
if bdata is None:
return None
name = bdata.name
if bdata.library is not None:
name += '_' + bdata.library.name
return name
def get_source_path(bdata) -> Optional[Path]:
"""Get Sources folder path for a linked data block's project."""
lib_path = get_library_path(bdata)
if lib_path is None:
return None
sources_path = lib_path.parent / 'Sources'
if sources_path.exists() and sources_path.is_dir():
return sources_path
return None
class TransformEvaluator:
"""Context manager for evaluating linked object transforms (Blender 4.2+ workaround)."""
def __init__(self, bobject: bpy.types.Object, scene: bpy.types.Scene,
depsgraph: bpy.types.Depsgraph):
self.bobject = bobject
self.scene = scene
self.depsgraph = depsgraph
self._temp_collection: Optional[bpy.types.Collection] = None
self._evaluated_obj: Optional[bpy.types.Object] = None
self._is_linked = False
def __enter__(self) -> 'TransformEvaluator':
if bpy.app.version >= (4, 2, 0):
self._is_linked = self.bobject.name not in self.scene.collection.children
if self._is_linked:
self._temp_collection = bpy.data.collections.new("_lnx_temp_eval")
bpy.context.scene.collection.children.link(self._temp_collection)
self._temp_collection.objects.link(self.bobject)
temp_depsgraph = bpy.context.evaluated_depsgraph_get()
self._evaluated_obj = self.bobject.evaluated_get(temp_depsgraph)
else:
self._evaluated_obj = self.bobject.evaluated_get(self.depsgraph)
else:
self._evaluated_obj = self.bobject
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if self._is_linked and self._temp_collection is not None:
try:
self._temp_collection.objects.unlink(self.bobject)
bpy.context.scene.collection.children.unlink(self._temp_collection)
bpy.data.collections.remove(self._temp_collection)
except Exception:
pass
self._temp_collection = None
return False
@property
def evaluated_object(self) -> bpy.types.Object:
return self._evaluated_obj
@property
def matrix_local(self):
if bpy.app.version >= (4, 2, 0):
return self._evaluated_obj.matrix_local.copy()
return self.bobject.matrix_local
@contextmanager
def evaluated_mesh(bobject: bpy.types.Object, scene: bpy.types.Scene,
depsgraph: bpy.types.Depsgraph, apply_modifiers: bool = True):
"""Context manager for mesh export with Blender 4.2+ linked object workaround."""
temp_collection = None
is_linked = False
try:
if apply_modifiers and bpy.app.version >= (4, 2, 0):
is_linked = bobject.name not in scene.collection.children
if is_linked:
temp_collection = bpy.data.collections.new("_lnx_temp_mesh_eval")
bpy.context.scene.collection.children.link(temp_collection)
temp_collection.objects.link(bobject)
temp_depsgraph = bpy.context.evaluated_depsgraph_get()
bobject_eval = bobject.evaluated_get(temp_depsgraph)
yield bobject_eval, temp_depsgraph
finally:
if is_linked and temp_collection is not None:
try:
temp_collection.objects.unlink(bobject)
bpy.context.scene.collection.children.unlink(temp_collection)
bpy.data.collections.remove(temp_collection)
except Exception:
pass
def discover_linked_sources() -> Dict[str, Path]:
"""Discover Sources folders from all linked libraries."""
sources: Dict[str, Path] = {}
for lib in bpy.data.libraries:
lib_path = Path(bpy.path.abspath(lib.filepath))
sources_path = lib_path.parent / 'Sources'
if sources_path.exists() and sources_path.is_dir():
sources[lib.name] = sources_path
return sources

View File

@ -28,6 +28,8 @@ def init_categories():
lnx_nodes.add_category('Logic', icon='OUTLINER', section="basic", lnx_nodes.add_category('Logic', icon='OUTLINER', section="basic",
description="Logic nodes are used to control execution flow using branching, loops, gates etc.") description="Logic nodes are used to control execution flow using branching, loops, gates etc.")
lnx_nodes.add_category('Event', icon='INFO', section="basic") lnx_nodes.add_category('Event', icon='INFO', section="basic")
lnx_nodes.add_category('Signal', icon='LINKED', section="basic",
description="Signal nodes provide type-safe, instance-based event communication between traits.")
lnx_nodes.add_category('Input', icon='GREASEPENCIL', section="basic") lnx_nodes.add_category('Input', icon='GREASEPENCIL', section="basic")
lnx_nodes.add_category('Native', icon='MEMORY', section="basic", lnx_nodes.add_category('Native', icon='MEMORY', section="basic",
description="The Native category contains nodes which interact with the system (Input/Output functionality, etc.) or Haxe.") description="The Native category contains nodes which interact with the system (Input/Output functionality, etc.) or Haxe.")

View File

@ -0,0 +1,18 @@
from lnx.logicnode.lnx_nodes import *
class GetTilesheetFlipNode(LnxLogicTreeNode):
"""Returns the flip state of the tilesheet.
@output Flip X: Whether the sprite is flipped horizontally.
@output Flip Y: Whether the sprite is flipped vertically.
"""
bl_idname = 'LNGetTilesheetFlipNode'
bl_label = 'Get Tilesheet Flip'
lnx_version = 1
lnx_section = 'tilesheet'
def lnx_init(self, context):
self.add_input('LnxNodeSocketObject', 'Object')
self.add_output('LnxBoolSocket', 'Flip X')
self.add_output('LnxBoolSocket', 'Flip Y')

View File

@ -3,7 +3,7 @@ from lnx.logicnode.lnx_nodes import *
class GetTilesheetStateNode(LnxLogicTreeNode): class GetTilesheetStateNode(LnxLogicTreeNode):
"""Returns the information about the current tilesheet of the given object. """Returns the information about the current tilesheet of the given object.
@output Active Tilesheet: Current active tilesheet. @output Tilesheet: Tilesheet name.
@output Active Action: Current action in the tilesheet. @output Active Action: Current action in the tilesheet.
@ -15,23 +15,28 @@ class GetTilesheetStateNode(LnxLogicTreeNode):
""" """
bl_idname = 'LNGetTilesheetStateNode' bl_idname = 'LNGetTilesheetStateNode'
bl_label = 'Get Tilesheet State' bl_label = 'Get Tilesheet State'
lnx_version = 2 lnx_version = 4
lnx_section = 'tilesheet' lnx_section = 'tilesheet'
def lnx_init(self, context): def lnx_init(self, context):
self.add_input('LnxNodeSocketObject', 'Object') self.add_input('LnxNodeSocketObject', 'Object')
self.add_output('LnxStringSocket', 'Active Tilesheet') self.add_output('LnxStringSocket', 'Tilesheet')
self.add_output('LnxStringSocket', 'Active Action') self.add_output('LnxStringSocket', 'Active Action')
self.add_output('LnxIntSocket', 'Frame') self.add_output('LnxIntSocket', 'Frame')
self.add_output('LnxIntSocket', 'Absolute Frame') self.add_output('LnxIntSocket', 'Absolute Frame')
self.add_output('LnxBoolSocket', 'Is Paused') self.add_output('LnxBoolSocket', 'Is Paused')
def get_replacement_node(self, node_tree: bpy.types.NodeTree): def get_replacement_node(self, node_tree: bpy.types.NodeTree):
if self.lnx_version not in (0, 1): if self.lnx_version in (0, 1):
raise LookupError()
return NodeReplacement( return NodeReplacement(
'LNGetTilesheetStateNode', self.lnx_version, 'LNGetTilesheetStateNode', 2, 'LNGetTilesheetStateNode', self.lnx_version, 'LNGetTilesheetStateNode', 4,
in_socket_mapping={}, out_socket_mapping={0: 1, 1: 3, 2: 4} in_socket_mapping={}, out_socket_mapping={0: 1, 1: 3, 2: 4}
) )
elif self.lnx_version in (2, 3):
# Version 2 and 3 have same outputs, just rename Material to Tilesheet
return NodeReplacement(
'LNGetTilesheetStateNode', self.lnx_version, 'LNGetTilesheetStateNode', 4,
in_socket_mapping={0: 0}, out_socket_mapping={0: 0, 1: 1, 2: 2, 3: 3, 4: 4}
)
raise LookupError()

View File

@ -1,16 +1,16 @@
from lnx.logicnode.lnx_nodes import * from lnx.logicnode.lnx_nodes import *
class PlayTilesheetNode(LnxLogicTreeNode): class PlayTilesheetActionNode(LnxLogicTreeNode):
"""Plays the given tilesheet action.""" """Plays the given tilesheet action."""
bl_idname = 'LNPlayTilesheetNode' bl_idname = 'LNPlayTilesheetActionNode'
bl_label = 'Play Tilesheet' bl_label = 'Play Tilesheet Action'
lnx_version = 1 lnx_version = 1
lnx_section = 'tilesheet' lnx_section = 'tilesheet'
def lnx_init(self, context): def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In') self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxNodeSocketObject', 'Object') self.add_input('LnxNodeSocketObject', 'Object')
self.add_input('LnxStringSocket', 'Name') self.add_input('LnxStringSocket', 'Action')
self.add_output('LnxNodeSocketAction', 'Out') self.add_output('LnxNodeSocketAction', 'Out')
self.add_output('LnxNodeSocketAction', 'Done') self.add_output('LnxNodeSocketAction', 'Done')

View File

@ -1,16 +1,15 @@
from lnx.logicnode.lnx_nodes import * from lnx.logicnode.lnx_nodes import *
class SetActiveTilesheetNode(LnxLogicTreeNode): class SetTilesheetActionNode(LnxLogicTreeNode):
"""Set the active tilesheet.""" """Sets the tilesheet action for the given object."""
bl_idname = 'LNSetActiveTilesheetNode' bl_idname = 'LNSetTilesheetActionNode'
bl_label = 'Set Active Tilesheet' bl_label = 'Set Tilesheet Action'
lnx_version = 1 lnx_version = 1
lnx_section = 'tilesheet' lnx_section = 'tilesheet'
def lnx_init(self, context): def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In') self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxNodeSocketObject', 'Object') self.add_input('LnxNodeSocketObject', 'Object')
self.add_input('LnxStringSocket', 'Tilesheet')
self.add_input('LnxStringSocket', 'Action') self.add_input('LnxStringSocket', 'Action')
self.add_output('LnxNodeSocketAction', 'Out') self.add_output('LnxNodeSocketAction', 'Out')

Some files were not shown because too many files have changed in this diff Show More