diff --git a/api/api.hxml b/api/api.hxml index 43437425..7f49883d 100644 --- a/api/api.hxml +++ b/api/api.hxml @@ -39,6 +39,8 @@ -D lnx_skin -D lnx_morph_target -D lnx_particles +-D lnx_cpu_particles +-D lnx_gpu_particles -D sys_krom -D sys_g1 -D sys_g2 diff --git a/leenkx.py b/leenkx.py index e8a7d38c..fef26a2f 100644 --- a/leenkx.py +++ b/leenkx.py @@ -6,8 +6,8 @@ bl_info = { "location": "Properties -> Render -> Leenkx Player", "description": "Full Stack SDK", "author": "Leenkx.com", - "version": (1, 0, 8), - "blender": (4, 2, 1), + "version": (2026, 5, 0), + "blender": (4, 5, 0), "doc_url": "https://leenkx.com/", "tracker_url": "https://leenkx.com/support" } @@ -318,7 +318,7 @@ class LeenkxAddonPreferences(AddonPreferences): layout.label(text="Welcome to Leenkx!") # 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.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, "Krom/Krom.app/Contents/MacOS/Krom"), # Kha tools - os.path.join(sdk, "Kha/Tools/macos/haxe"), - os.path.join(sdk, "Kha/Tools/macos/lame"), - os.path.join(sdk, "Kha/Tools/macos/oggenc"), + os.path.join(sdk, "Kha/Tools/macos_x64/haxe"), + os.path.join(sdk, "Kha/Tools/macos_x64/lame"), + os.path.join(sdk, "Kha/Tools/macos_x64/oggenc"), # Kinc tools - os.path.join(sdk, "Kha/Kinc/Tools/macos/kmake"), - os.path.join(sdk, "Kha/Kinc/Tools/macos/kraffiti"), - os.path.join(sdk, "Kha/Kinc/Tools/macos/krafix"), + os.path.join(sdk, "Kha/Kinc/Tools/macos_x64/kmake"), + os.path.join(sdk, "Kha/Kinc/Tools/macos_x64/kraffiti"), + os.path.join(sdk, "Kha/Kinc/Tools/macos_x64/krafix"), ] for path in paths: os.chmod(path, 0o777) diff --git a/leenkx/Shaders/std/brdf.glsl b/leenkx/Shaders/std/brdf.glsl index 8273419e..9c6e57b2 100644 --- a/leenkx/Shaders/std/brdf.glsl +++ b/leenkx/Shaders/std/brdf.glsl @@ -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) { 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 - return min(result, vec3(200.0)); + return result; } // 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) { - return albedo * nl; + return albedo * (1.0 / 3.1415926535) * nl; } vec3 surfaceAlbedo(const vec3 baseColor, const float metalness) { diff --git a/leenkx/Shaders/std/light.glsl b/leenkx/Shaders/std/light.glsl index c9738daf..17bb2266 100644 --- a/leenkx/Shaders/std/light.glsl +++ b/leenkx/Shaders/std/light.glsl @@ -131,7 +131,7 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co #ifdef _SSRS , sampler2D gbufferD, mat4 invVP, vec3 eye #endif - ) { +) { vec3 ld = lp - p; vec3 l = normalize(ld); vec3 h = normalize(v + l); @@ -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(0.0, t.z, 0.0), 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; - 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; #else 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 if (receiveShadow) { #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], #ifdef _ShadowMapTransparent shadowMapSpotTransparent[0], @@ -193,7 +194,7 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co ); #endif #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], #ifdef _ShadowMapTransparent 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(0.0, t.z, 0.0), 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; - 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; #else vec3 direct = lambertDiffuseBRDF(albedo, dotNL) + diff --git a/leenkx/Sources/iron/App.hx b/leenkx/Sources/iron/App.hx index 46319cef..796fe453 100644 --- a/leenkx/Sources/iron/App.hx +++ b/leenkx/Sources/iron/App.hx @@ -24,11 +24,11 @@ class App { public static var renderPathTime: Float; public static var endFrameCallbacks: ArrayVoid> = []; #end - static var last = 0.0; static var time = 0.0; static var lastw = -1; static var lasth = -1; - public static var onResize: Void->Void = null; + public static var onResize: Void->Void = null; // TODO: deprecate this. Use leenkx.system.Signal 'resized' instead. + public static var resized: leenkx.system.Signal = new leenkx.system.Signal(); // args: (w: Int, h: Int) public static function init(done: Void->Void) { new App(done); @@ -75,6 +75,7 @@ class App { lasth = App.h(); } if (lastw != App.w() || lasth != App.h()) { + resized.emit(App.w(), App.h()); if (onResize != null) onResize(); else { if (Scene.active != null && Scene.active.camera != null) { @@ -93,7 +94,6 @@ class App { Scene.active.updateFrame(); - time += iron.system.Time.delta; while (time >= iron.system.Time.fixedStep) { @@ -101,16 +101,19 @@ class App { time -= iron.system.Time.fixedStep; } + @:privateAccess iron.system.Time._fixedStepInterpolation = time / iron.system.Time.fixedStep; + var i = 0; var l = traitUpdates.length; while (i < l) { - if (traitInits.length > 0) { - for (f in traitInits) { - traitInits.length > 0 ? f() : break; - } - traitInits.splice(0, traitInits.length); + while (traitInits.length > 0) { + var f = traitInits.shift(); + if (f != null) f(); + } + // Re-check bounds after processing inits (scene switch may have removed traits) + if (i < traitUpdates.length) { + traitUpdates[i](); } - traitUpdates[i](); // Account for removed traits l <= traitUpdates.length ? i++ : l = traitUpdates.length; } @@ -146,11 +149,9 @@ class App { startTime = kha.Scheduler.realTime(); #end - if (traitInits.length > 0) { - for (f in traitInits) { - traitInits.length > 0 ? f() : break; - } - traitInits.splice(0, traitInits.length); + while (traitInits.length > 0) { + var f = traitInits.shift(); + if (f != null) f(); } // skip for XR callback to handle rendering diff --git a/leenkx/Sources/iron/RenderPath.hx b/leenkx/Sources/iron/RenderPath.hx index 2f35c0de..18a85cee 100644 --- a/leenkx/Sources/iron/RenderPath.hx +++ b/leenkx/Sources/iron/RenderPath.hx @@ -25,7 +25,6 @@ import iron.math.Quat; #end class RenderPath { - public static var active: RenderPath; #if lnx_vr static var vrSimulateMode: Bool = false; @@ -446,7 +445,9 @@ class RenderPath { public function clearTarget(colorFlag: Null = null, depthFlag: Null = null) { if (colorFlag == -1) { // -1 == 0xffffffff if (Scene.active.world != null) { - colorFlag = Scene.active.world.raw.background_color; + var col = Scene.active.world.raw.background_color; + var strength = Scene.active.world.probe != null ? Scene.active.world.probe.raw.strength : 1.0; + colorFlag = Color.fromFloats(((col >> 16) & 0xff) / 255 * strength, ((col >> 8) & 0xff) / 255 * strength, (col & 0xff) / 255 * strength); } else if (Scene.active.camera != null) { var cc = Scene.active.camera.data.raw.clear_color; @@ -477,7 +478,8 @@ class RenderPath { if (depthDiff != 0) return depthDiff; #end - return a.cameraDistance >= b.cameraDistance ? 1 : -1; + if (a.cameraDistance == b.cameraDistance) return 0; + return a.cameraDistance > b.cameraDistance ? 1 : -1; }); } @@ -491,7 +493,9 @@ class RenderPath { if (a.data.sortingIndex != b.data.sortingIndex) { return a.data.sortingIndex > b.data.sortingIndex ? 1 : -1; } - return a.data.name >= b.data.name ? 1 : -1; + + if (a.data.name == b.data.name) return 0; + return a.data.name > b.data.name ? 1 : -1; }); } @@ -667,9 +671,11 @@ class RenderPath { end(); } + #if (rp_voxels != "Off") public function getComputeShader(handle: String): kha.compute.Shader { return Reflect.field(kha.Shaders, handle + "_comp"); } + #end #if lnx_vr // blits to each eyes viewport in the XR framebuffer. @@ -1229,7 +1235,7 @@ class CachedShaderContext { public function new() {} } -@:enum abstract DrawOrder(Int) from Int { +enum abstract DrawOrder(Int) from Int { var Distance = 0; // Early-z var Index = 1; // Less state changes // var Mix = 2; // Distance buckets sorted by shader diff --git a/leenkx/Sources/iron/Scene.hx b/leenkx/Sources/iron/Scene.hx index 0a4a25a1..734165df 100644 --- a/leenkx/Sources/iron/Scene.hx +++ b/leenkx/Sources/iron/Scene.hx @@ -64,7 +64,6 @@ class Scene { #end public var empties: Array; public var animations: Array; - public var tilesheets: Array; #if lnx_skin public var armatures: Array; #end @@ -78,6 +77,9 @@ class Scene { public var traitRemoves: ArrayVoid> = []; var initializing: Bool; // Is the scene in its initialization phase? + var spawnDepth: Int = 0; // Nested spawn counter (defer trait creation while > 0) + var spawning(get, never): Bool; + inline function get_spawning(): Bool return spawnDepth > 0; public function new() { uid = uidCounter++; @@ -101,7 +103,6 @@ class Scene { #end empties = []; animations = []; - tilesheets = []; #if lnx_skin armatures = []; #end @@ -124,9 +125,8 @@ class Scene { // Startup scene active.addScene(format.name, null, function(sceneObject: Object) { - for (object in sceneObject.getChildren(true)) { - createTraits(object.raw.traits, object); - } + // Create traits bottom-up (children first, then parents) + createTraitsBottomUp(sceneObject); #if lnx_terrain if (format.terrain_ref != null) { @@ -141,17 +141,29 @@ class Scene { active.camera = active.getCamera(format.camera_ref); active.sceneParent = sceneObject; - active.ready = true; - for (f in active.traitInits) f(); active.traitInits = []; + active.ready = true; active.initializing = false; done(sceneObject); }); }); } + // Create traits in post-order (bottom-up): children's traits are created before parents. + // This ensures that when a parent's notifyOnInit runs, children are already initialized. + static function createTraitsBottomUp(object: Object) { + // First, recursively process all children + for (child in object.children) { + createTraitsBottomUp(child); + } + // Then create traits for this object + if (object.raw != null) { + createTraits(object.raw.traits, object); + } + } + #if lnx_patch public static var getRenderPath: Void->RenderPath; public static function patch() { @@ -212,6 +224,8 @@ class Scene { Data.getSceneRaw(sceneName, function(format: TSceneFormat) { Scene.create(format, function(o: Object) { + framePassed = true; + if (done != null) done(o); #if (rp_background == "World") @@ -242,10 +256,6 @@ class Scene { if (!ready || RenderPath.active == null) return; framePassed = true; - for (tilesheet in tilesheets) { - tilesheet.update(); - } - // Render probes #if rp_probes var activeCamera = camera; @@ -289,33 +299,39 @@ class Scene { return root.children.length > 0 ? root.children[0].getTrait(c) : null; } + // TODO: solve name referencing for linked objects public function getMesh(name: String): MeshObject { for (m in meshes) if (m.name == name) return m; return null; } + // TODO: solve name referencing for linked objects public function getLight(name: String): LightObject { for (l in lights) if (l.name == name) return l; return null; } + // TODO: solve name referencing for linked objects public function getCamera(name: String): CameraObject { for (c in cameras) if (c.name == name) return c; return null; } #if lnx_audio + // TODO: solve name referencing for linked objects public function getSpeaker(name: String): SpeakerObject { for (s in speakers) if (s.name == name) return s; return null; } #end + // TODO: solve name referencing for linked objects public function getEmpty(name: String): Object { for (e in empties) if (e.name == name) return e; return null; } + // TODO: solve name referencing for linked objects public function getGroup(name: String): Array { if (groups == null) groups = new Map(); var g = groups.get(name); @@ -391,6 +407,7 @@ class Scene { #end var objectsCount = getObjectsCount(format.objects); + spawnDepth++; // Defer trait creation until all objects are ready function traverseObjects(parent: Object, objects: Array, parentObject: TObj, done: Void->Void) { if (objects == null) return; for (i in 0...objects.length) { @@ -408,11 +425,16 @@ class Scene { } if (format.objects == null || format.objects.length == 0) { + spawnDepth--; createTraits(format.traits, parent); // Scene traits done(parent); } else { traverseObjects(parent, format.objects, null, function() { // Scene objects + spawnDepth--; + if (!initializing) { + createTraitsBottomUp(parent); + } createTraits(format.traits, parent); // Scene traits done(parent); }); @@ -426,7 +448,7 @@ class Scene { var result = objects.length; for (o in objects) { if (discardNoSpawn && o.spawn != null && o.spawn == false) continue; // Do not count children of non-spawned objects - if (o.children != null) result += getObjectsCount(o.children); + if (o.children != null) result += getObjectsCount(o.children, discardNoSpawn); } return result; } @@ -440,11 +462,16 @@ class Scene { @param srcRaw If not `null`, spawn the object from the given scene data instead of using the scene this function is called on. Useful to spawn objects from other scenes. **/ public function spawnObject(name: String, parent: Null, done: NullVoid>, spawnChildren = true, srcRaw: Null = null) { + spawnObjectInternal(name, parent, done, spawnChildren, srcRaw, true); + } + + function spawnObjectInternal(name: String, parent: Null, done: NullVoid>, spawnChildren: Bool, srcRaw: Null, createTraits: Bool) { if (srcRaw == null) srcRaw = raw; var objectsTraversed = 0; var obj = getRawObjectByName(srcRaw, name); var objectsCount = spawnChildren ? getObjectsCount([obj], false) : 1; var rootId = -1; + spawnDepth++; // Defer trait creation until all objects are ready function spawnObjectTree(obj: TObj, parent: Object, parentObject: TObj, done: Object->Void) { createObject(obj, srcRaw, parent, parentObject, function(object: Object) { if (rootId == -1) { @@ -453,20 +480,27 @@ class Scene { if (spawnChildren && obj.children != null) { for (child in obj.children) spawnObjectTree(child, object, obj, done); } - if (++objectsTraversed == objectsCount && done != null) { + if (++objectsTraversed == objectsCount) { // Retrieve the originally spawned object from the current // child object to ensure done() is called with the right // object while (object.uid != rootId) { object = object.parent; } - done(object); + // Create traits bottom-up after all objects are ready + spawnDepth--; + if (createTraits) { + createTraitsBottomUp(object); + } + // Then call user callback + if (done != null) done(object); } }); } spawnObjectTree(obj, parent, null, done); } + // TODO: solve name referencing for linked objects public function parseObject(sceneName: String, objectName: String, parent: Object, done: Object->Void) { Data.getSceneRaw(sceneName, function(format: TSceneFormat) { var o: TObj = getRawObjectByName(format, objectName); @@ -495,6 +529,10 @@ class Scene { static function traverseObjs(children: Array, name: String): TObj { for (o in children) { if (o.name == name) return o; + else if (o.filename != "") { + var n: String = name + "_" + o.filename; + if (o.name == n) return o; + } if (o.children != null) { var res = traverseObjs(o.children, name); if (res != null) return res; @@ -592,7 +630,7 @@ class Scene { else { for (object_ref in object_refs) { // Spawn top-level collection objects and their children - spawnObject(object_ref, groupOwner, function(spawnedObject: Object) { + spawnObjectInternal(object_ref, groupOwner, function(spawnedObject: Object) { // Apply collection/group instance offset to all // top-level parents of that group if (!isObjectInGroup(groupRef, spawnedObject.parent, format)) { @@ -610,9 +648,10 @@ class Scene { } if (++spawned == object_refs.length) { groupOwner.transform.reset(); + groupOwner.transform.buildMatrix(); done(); } - }, true, format); + }, true, format, false); } } } @@ -764,6 +803,7 @@ class Scene { #end } + // TODO: solve name referencing for linked objects public function returnMeshObject(object_file: String, data_ref: String, sceneName: String, armature: #if lnx_skin Armature #else Null #end, materials: Vector, parent: Object, parentObject: TObj, o: TObj, done: Object->Void) { Data.getMesh(object_file, data_ref, function(mesh: MeshData) { var object = addMeshObject(mesh, materials, parent); @@ -779,9 +819,9 @@ class Scene { for (ref in o.particle_refs) cast(object, MeshObject).setupParticleSystem(sceneName, ref); } #end - // Attach tilesheet - if (o.tilesheet_ref != null) { - cast(object, MeshObject).setupTilesheet(sceneName, o.tilesheet_ref, o.tilesheet_action_ref); + // Attach tilesheet from embedded object data + if (o.tilesheet != null) { + cast(object, MeshObject).setupTilesheet(o.tilesheet); } if (o.camera_list != null){ @@ -820,6 +860,7 @@ class Scene { if (object != null) { object.raw = o; object.name = o.name; + if (o.filename != null) object.filename = o.filename; if (o.visible != null) object.visible = o.visible; if (o.visible_mesh != null) object.visibleMesh = o.visible_mesh; if (o.visible_shadow != null) object.visibleShadow = o.visible_shadow; @@ -848,9 +889,9 @@ class Scene { } } - // If the scene is still initializing, traits will be created later + // If the scene is still initializing or spawning, traits will be created later // to ensure that object references for trait properties are valid - if (!active.initializing) createTraits(o.traits, object); + if (!active.initializing && !active.spawning) createTraits(o.traits, object); } done(object); } @@ -882,17 +923,15 @@ class Scene { // Set trait properties if (t.props != null) { + var traitFields = Type.getInstanceFields(Type.getClass(traitInst)); for (i in 0...Std.int(t.props.length / 3)) { var pname: String = t.props[i * 3]; var ptype: String = t.props[i * 3 + 1]; var pval: Dynamic = t.props[i * 3 + 2]; + if (traitFields.indexOf(pname) == -1) continue; if (StringTools.endsWith(ptype, "Object") && pval != "" && pval != null) { Reflect.setProperty(traitInst, pname, Scene.active.getChild(pval)); - } else if (ptype == "TSceneFormat" && pval != "") { - Data.getSceneRaw(pval, function (r: TSceneFormat) { - Reflect.setProperty(traitInst, pname, r); - }); } else { switch (ptype) { diff --git a/leenkx/Sources/iron/Trait.hx b/leenkx/Sources/iron/Trait.hx index dd63b001..c3164943 100644 --- a/leenkx/Sources/iron/Trait.hx +++ b/leenkx/Sources/iron/Trait.hx @@ -14,9 +14,9 @@ class Trait { var _add: ArrayVoid> = null; var _init: ArrayVoid> = null; var _remove: ArrayVoid> = null; + var _fixedUpdate: ArrayVoid> = null; var _update: ArrayVoid> = null; var _lateUpdate: ArrayVoid> = null; - var _fixedUpdate: ArrayVoid> = null; var _render: ArrayVoid> = null; var _render2D: ArrayVoid> = null; diff --git a/leenkx/Sources/iron/data/Data.hx b/leenkx/Sources/iron/data/Data.hx index 5dcc8ed7..bb17a0e8 100644 --- a/leenkx/Sources/iron/data/Data.hx +++ b/leenkx/Sources/iron/data/Data.hx @@ -416,7 +416,7 @@ class Data { var loading = loadingBlobs.get(file); // Is already being loaded if (loading != null) { loading.push(done); - return; + //return; } loadingBlobs.set(file, [done]); // Start loading diff --git a/leenkx/Sources/iron/data/SceneFormat.hx b/leenkx/Sources/iron/data/SceneFormat.hx index dd080a8e..9b5bdf30 100644 --- a/leenkx/Sources/iron/data/SceneFormat.hx +++ b/leenkx/Sources/iron/data/SceneFormat.hx @@ -31,7 +31,6 @@ typedef TSceneFormat = { @:optional public var speaker_datas: Array; @:optional public var world_datas: Array; @:optional public var world_ref: String; - @:optional public var tilesheet_datas: Array; @:optional public var objects: Array; @:optional public var groups: Array; @:optional public var gravity: Float32Array; @@ -169,6 +168,7 @@ typedef TShaderOverride = { @:structInit class TShaderOverride { #end @:optional public var cull_mode: String; + @:optional public var compare_mode: String; @:optional public var addressing: String; @:optional public var filter: String; @:optional public var shared_sampler: String; @@ -364,18 +364,6 @@ typedef TProbeData = { @:optional public var radiance_mipmaps: Null; } -#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; -} - #if js typedef TTilesheetAction = { #else @@ -385,6 +373,31 @@ typedef TTilesheetAction = { public var start: Int; public var end: Int; public var loop: Bool; + public var tilesx: Int; + public var tilesy: Int; + public var framerate: Int; + @:optional public var mesh: String; // Optional mesh to swap to when playing this action + @:optional public var events: Array; // 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; + public var start_action: String; + public var flipx: Bool; + public var flipy: Bool; } #if js @@ -392,26 +405,47 @@ typedef TParticleData = { #else @:structInit class TParticleData { #end + // Format + public var fps: Int; public var name: String; public var type: Int; // 0 - Emitter, Hair + // Lnx public var auto_start: Bool; public var dynamic_emitter: Bool; public var is_unique: Bool; + public var local_coords: Bool; public var loop: Bool; + // Emission public var count: Int; + // public var hair_length: FastFloat; TODO public var frame_start: FastFloat; public var frame_end: FastFloat; public var lifetime: FastFloat; public var lifetime_random: FastFloat; public var emit_from: Int; // 0 - Vert, 1 - Face, 2 - Volume + // Velocity public var object_align_factor: Float32Array; public var factor_random: FastFloat; + // Rotation + public var use_rotations: Bool; + public var rotation_mode: Int; // 0 - None, 1 - Normal, 2 - Normal-Tangent, 3 - Velocity/Hair, 4 - Global X, 5 - Global Y, 6 - Global Z, 7 - Object X, 8 - Object Y, 9 - Object Z + public var rotation_factor_random: Float; + public var phase_factor: Float; + public var phase_factor_random: Float; + public var use_dynamic_rotation: Bool; + // Physics public var physics_type: Int; // 0 - No, 1 - Newton + public var mass: FastFloat; + // Render public var particle_size: FastFloat; // Object scale public var size_random: FastFloat; // Random scale - public var mass: FastFloat; + public var show_emitter: Bool; public var instance_object: String; // Object reference + // Field Weights public var weight_gravity: FastFloat; + public var weight_texture: FastFloat; + // Textures + public var texture_slots: Dynamic; } #if js @@ -433,6 +467,7 @@ typedef TObj = { public var name: String; public var data_ref: String; public var transform: TTransform; + @:optional public var filename: String; // For objects instanced from external files @:optional public var material_refs: Array; @:optional public var particle_refs: Array; @:optional public var render_emitter: Bool; @@ -462,8 +497,7 @@ typedef TObj = { @:optional public var mobile: Null; @:optional public var spawn: Null; // Auto add object when creating scene @:optional public var local_only: Null; // Apply parent matrix - @:optional public var tilesheet_ref: String; - @:optional public var tilesheet_action_ref: String; + @:optional public var tilesheet: TTilesheetData; // Embedded tilesheet data @:optional public var sampled: Null; // Object action @:optional public var is_ik_fk_only: Null; // Bone IK or FK only @:optional public var bone_layers: Array; // Bone Layer diff --git a/leenkx/Sources/iron/data/ShaderData.hx b/leenkx/Sources/iron/data/ShaderData.hx index 7ceb98b1..b099e19e 100644 --- a/leenkx/Sources/iron/data/ShaderData.hx +++ b/leenkx/Sources/iron/data/ShaderData.hx @@ -230,6 +230,9 @@ class ShaderContext { if (overrideContext.cull_mode != null) { pipeState.cullMode = getCullMode(overrideContext.cull_mode); } + if (overrideContext.compare_mode != null) { + pipeState.depthMode = getCompareMode(overrideContext.compare_mode); + } } pipeState.compile(); diff --git a/leenkx/Sources/iron/data/TerrainStream.hx b/leenkx/Sources/iron/data/TerrainStream.hx index cdf314ee..45590363 100644 --- a/leenkx/Sources/iron/data/TerrainStream.hx +++ b/leenkx/Sources/iron/data/TerrainStream.hx @@ -114,11 +114,11 @@ class TerrainStream { var rawmeshData: TMeshData = { name: "Terrain", - sorting_index: 0, vertex_arrays: [pos, nor, tex], index_arrays: [ind], scale_pos: scalePos, - scale_tex: 1.0 + scale_tex: 1.0, + sorting_index: 0 }; new MeshData(rawmeshData, function(data: MeshData) { diff --git a/leenkx/Sources/iron/object/Animation.hx b/leenkx/Sources/iron/object/Animation.hx index 55bed4e3..a5056d2d 100644 --- a/leenkx/Sources/iron/object/Animation.hx +++ b/leenkx/Sources/iron/object/Animation.hx @@ -14,7 +14,7 @@ class Animation { public var isSkinned: Bool; public var isSampled: Bool; - public var action = ""; + @:isVar public var action(get, default) = ""; #if lnx_skin public var armature: iron.data.Armature; // Bone #end @@ -57,6 +57,10 @@ class Animation { play(); } + function get_action(): String { + return action; + } + public function play(action = "", onComplete: Void->Void = null, blendTime = 0.0, speed = 1.0, loop = true) { if (blendTime > 0) { this.blendTime = blendTime; @@ -98,7 +102,7 @@ class Animation { else { sampler.timeOld = sampler.time; sampler.offsetOld = sampler.offset; - sampler.setTimeOnly(sampler.time + delta * sampler.speed); + sampler.setTimeOnly(sampler.time + delta * sampler.speed * iron.system.Time.scale); updateActionTrack(sampler); } } diff --git a/leenkx/Sources/iron/object/BoneAnimation.hx b/leenkx/Sources/iron/object/BoneAnimation.hx index 21c866f7..1f5a2bbf 100644 --- a/leenkx/Sources/iron/object/BoneAnimation.hx +++ b/leenkx/Sources/iron/object/BoneAnimation.hx @@ -14,6 +14,8 @@ import iron.data.Armature; import iron.data.Data; import iron.math.Ray; +import StringTools; + class BoneAnimation extends Animation { public static var skinMaxBones = 128; @@ -84,6 +86,15 @@ class BoneAnimation extends Animation { } } + override function get_action(): String { + var an: String = action; // an -> action name + if (an != "" && object != null && object.filename != "") { + var sufix = "_" + object.filename; + if (an.indexOf(sufix) != -1) an = StringTools.replace(an, sufix, ""); + } + return an; + } + public function initMatsEmpty(): Array { var mats = []; @@ -135,7 +146,16 @@ class BoneAnimation extends Animation { if (ar != null) ar.remove(o); } } - + + function getName(n: String): String { + var fn: String = n; // fn -> final name + if (fn != "" && object != null && object.filename != "") { + var sufix = "_" + object.filename; + if (fn.indexOf(sufix) == -1) fn += sufix; + } + return fn; + } + @:access(iron.object.Transform) function updateBoneChildren(bone: TObj, bm: Mat4) { var ar = boneChildren.get(bone.name); @@ -232,10 +252,11 @@ class BoneAnimation extends Animation { } override public function play(action = "", onComplete: Void->Void = null, blendTime = 0.2, speed = 1.0, loop = true) { - if (action != "") { - setAction(action); - super.play(action, onComplete, blendTime, speed, loop); - var tempAnimParam = new ActionSampler(action); + var actionName: String = getName(action); + if (actionName != "") { + setAction(actionName); + super.play(actionName, onComplete, blendTime, speed, loop); + var tempAnimParam = new ActionSampler(actionName); registerAction("tempAction", tempAnimParam); updateAnimation = function(mats){ sampleAction(tempAnimParam, mats); diff --git a/leenkx/Sources/iron/object/LightObject.hx b/leenkx/Sources/iron/object/LightObject.hx index cede9015..0749fab4 100644 --- a/leenkx/Sources/iron/object/LightObject.hx +++ b/leenkx/Sources/iron/object/LightObject.hx @@ -370,8 +370,6 @@ class LightObject extends Object { } #end // lnx_csm - #if lnx_clusters - // Centralize discarding conditions when iterating over lights // Important to avoid issues later with "misaligned" data in uniforms (lightsArray, clusterData, LWVPSpotArray) public inline static function discardLight(light: LightObject) { @@ -381,6 +379,8 @@ class LightObject extends Object { public inline static function discardLightCulled(light: LightObject) { return #if lnx_shadowmap_atlas light.culledLight || #end discardLight(light); } + + #if lnx_clusters #if (lnx_shadowmap_atlas && lnx_shadowmap_atlas_lod) // Arbitrary function to map from [0-16] to [1.0-0.0] @@ -422,7 +422,8 @@ class LightObject extends Object { #if lnx_spot // Point lamps first lights.sort(function(a, b): Int { - return a.data.raw.type >= b.data.raw.type ? 1 : -1; + if (a.data.raw.type == b.data.raw.type) return 0; + return a.data.raw.type > b.data.raw.type ? 1 : -1; }); #end @@ -494,6 +495,13 @@ class LightObject extends Object { continue; } #end + if (minX < 0) minX = 0; + if (maxX >= slicesX) maxX = slicesX - 1; + if (minY < 0) minY = 0; + if (maxY >= slicesY) maxY = slicesY - 1; + if (minZ < 0) minZ = 0; + if (maxZ >= slicesZ) maxZ = slicesZ - 1; + // Mark affected clusters for (z in minZ...maxZ + 1) { for (y in minY...maxY + 1) { diff --git a/leenkx/Sources/iron/object/MeshObject.hx b/leenkx/Sources/iron/object/MeshObject.hx index f378e256..5e724643 100644 --- a/leenkx/Sources/iron/object/MeshObject.hx +++ b/leenkx/Sources/iron/object/MeshObject.hx @@ -18,17 +18,18 @@ class MeshObject extends Object { public var depthRead(default, null) = false; #if lnx_particles public var particleSystems: Array = null; // Particle owner + public var render_emitter = true; + #end + #if lnx_gpu_particles public var particleChildren: Array = null; public var particleOwner: MeshObject = null; // Particle object public var particleIndex = -1; - public var render_emitter = true; #end public var cameraDistance: Float; public var cameraList: Array = null; public var screenSize = 0.0; public var frustumCulling = true; - public var activeTilesheet: Tilesheet = null; - public var tilesheets: Array = null; + public var tilesheet: Tilesheet = null; public var skip_context: String = null; // Do not draw this context public var force_context: String = null; // Draw only this context static var lastPipeline: PipelineState = null; @@ -72,18 +73,22 @@ class MeshObject extends Object { #if lnx_batch Scene.active.meshBatch.removeMesh(this); #end - #if lnx_particles + #if lnx_gpu_particles if (particleChildren != null) { for (c in particleChildren) c.remove(); particleChildren = null; } + #end + #if lnx_particles if (particleSystems != null) { - for (psys in particleSystems) psys.remove(); + for (psys in particleSystems) { + #if lnx_cpu_particles psys.stop(); #end + psys.remove(); + } particleSystems = null; } #end - if (activeTilesheet != null) activeTilesheet.remove(); - if (tilesheets != null) for (ts in tilesheets) { ts.remove(); } + if (tilesheet != null) tilesheet.remove(); if (Scene.active != null) Scene.active.meshes.remove(this); data.refcount--; super.remove(); @@ -113,35 +118,19 @@ class MeshObject extends Object { #if lnx_particles public function setupParticleSystem(sceneName: String, pref: TParticleReference) { if (particleSystems == null) particleSystems = []; - var psys = new ParticleSystem(sceneName, pref); + var psys = new ParticleSystem(sceneName, pref, this); particleSystems.push(psys); } #end - public function setupTilesheet(sceneName: String, tilesheet_ref: String, tilesheet_action_ref: String) { - activeTilesheet = new Tilesheet(sceneName, tilesheet_ref, tilesheet_action_ref); - if(tilesheets == null) tilesheets = new Array(); - tilesheets.push(activeTilesheet); + public function setupTilesheet(tilesheetData: iron.data.SceneFormat.TTilesheetData) { + tilesheet = new Tilesheet(tilesheetData, this); } - public function setActiveTilesheet(sceneName: String, tilesheet_ref: String, tilesheet_action_ref: String) { - var set = false; - // Check if tilesheet already created - if (tilesheets != null) { - for (ts in tilesheets) { - if (ts.raw.name == tilesheet_ref) { - activeTilesheet = ts; - activeTilesheet.play(tilesheet_action_ref); - set = true; - break; - } - } + public function setTilesheetAction(actionRef: String) { + if (tilesheet != null) { + tilesheet.play(actionRef); } - // If not already created - if (!set) { - setupTilesheet(sceneName, tilesheet_ref, tilesheet_action_ref); - } - } inline function isLodMaterial(): Bool { @@ -179,7 +168,7 @@ class MeshObject extends Object { // Scale radius for skinned mesh and particle system // TODO: define skin & particle bounds var radiusScale = data.isSkinned ? 2.0 : 1.0; - #if lnx_particles + #if lnx_gpu_particles // particleSystems for update, particleOwner for render if (particleSystems != null || particleOwner != null) radiusScale *= 1000; #end @@ -236,9 +225,14 @@ class MeshObject extends Object { if (cullMesh(context, Scene.active.camera, RenderPath.active.light)) return; var meshContext = raw != null ? context == "mesh" : false; + // Update tilesheet + if (tilesheet != null && meshContext) { + tilesheet.update(); + } + if (cameraList != null && cameraList.indexOf(Scene.active.camera.name) < 0) return; - #if lnx_particles + #if lnx_gpu_particles if (raw != null && raw.is_particle && particleOwner == null) return; // Instancing not yet set-up by particle system owner if (particleSystems != null && meshContext) { if (particleChildren == null) { @@ -257,9 +251,11 @@ class MeshObject extends Object { } } for (i in 0...particleSystems.length) { - particleSystems[i].update(particleChildren[i], this); + particleSystems[i].update(particleChildren[i]); } } + #end + #if lnx_particles if (particleSystems != null && particleSystems.length > 0 && !render_emitter) return; if (particleSystems == null && cullMaterial(context)) return; #else diff --git a/leenkx/Sources/iron/object/Object.hx b/leenkx/Sources/iron/object/Object.hx index 570eec50..6eca6f96 100644 --- a/leenkx/Sources/iron/object/Object.hx +++ b/leenkx/Sources/iron/object/Object.hx @@ -11,6 +11,7 @@ class Object { public var raw: TObj = null; public var name: String = ""; + public var filename: String = ""; public var transform: Transform; public var constraints: Array = null; public var traits: Array = []; @@ -111,12 +112,15 @@ class Object { **/ public function getChild(name: String): Object { if (this.name == name) return this; - else { - for (c in children) { - var r = c.getChild(name); - if (r != null) return r; - } + else if (this.filename != "") { + if (this.name == name + "_" + this.filename) return this; } + + for (c in children) { + var r = c.getChild(name); + if (r != null) return r; + } + return null; } @@ -209,6 +213,16 @@ class Object { return null; } + public function getTraitFromChildren(c: Class): T { + var t: T = getTrait(c); + if (t != null) return t; + for (child in getChildren(true)) { + t = child.getTraitFromChildren(c); + if (t != null) return t; + } + return null; + } + #if lnx_skin public function getBoneAnimation(armatureUid: Int): BoneAnimation { for (a in Scene.active.animations) { @@ -218,10 +232,21 @@ class Object { } return null; } + + public function getParentArmature(name: String): BoneAnimation { + for (a in Scene.active.animations) { + if (a.armature != null && a.armature.name == name) return cast a; + } + return null; + } #else public function getBoneAnimation(armatureUid): Animation { return null; } + + public function getParentArmature(name: String): Animation { + return null; + } #end public function getObjectAnimation(): ObjectAnimation { @@ -229,6 +254,15 @@ class Object { return null; } + public function getAnimation(): Null { + 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 = null) { // Parented to bone #if lnx_skin diff --git a/leenkx/Sources/iron/object/ObjectAnimation.hx b/leenkx/Sources/iron/object/ObjectAnimation.hx index 036282ab..198daaff 100644 --- a/leenkx/Sources/iron/object/ObjectAnimation.hx +++ b/leenkx/Sources/iron/object/ObjectAnimation.hx @@ -9,6 +9,7 @@ import iron.math.Vec4; import iron.math.Mat4; import iron.math.Quat; import iron.data.SceneFormat; +import StringTools; class ObjectAnimation extends Animation { @@ -42,13 +43,24 @@ class ObjectAnimation extends Animation { isSkinned = false; super(); } + + override function get_action(): String { + var an: String = action; // an -> action name + if (an != "" && object != null && object.filename != "") { + var sufix = "_" + object.filename; + if (an.indexOf(sufix) != -1) an = StringTools.replace(an, sufix, ""); + } + return an; + } + function getAction(action: String): TObj { for (a in oactions) if (a != null && a.objects[0].name == action) return a.objects[0]; return null; } override public function play(action = "", onComplete: Void->Void = null, blendTime = 0.0, speed = 1.0, loop = true) { - super.play(action, onComplete, blendTime, speed, loop); + var actionName: String = object != null && object.filename != "" ? action + "_" + object.filename : action; + super.play(actionName, onComplete, blendTime, speed, loop); if (this.action == "" && oactions != null && oactions[0] != null){ this.action = oactions[0].objects[0].name; } diff --git a/leenkx/Sources/iron/object/ParticleSystem.hx b/leenkx/Sources/iron/object/ParticleSystem.hx index bd66660a..5c13a5ed 100644 --- a/leenkx/Sources/iron/object/ParticleSystem.hx +++ b/leenkx/Sources/iron/object/ParticleSystem.hx @@ -1,461 +1,9 @@ package iron.object; -#if lnx_particles - -import kha.FastFloat; -import kha.graphics4.Usage; -import kha.arrays.Float32Array; -import iron.data.Data; -import iron.data.ParticleData; -import iron.data.SceneFormat; -import iron.data.Geometry; -import iron.data.MeshData; -import iron.system.Time; -import iron.math.Mat4; -import iron.math.Quat; -import iron.math.Vec3; -import iron.math.Vec4; - -class ParticleSystem { - public var data: ParticleData; - public var speed = 1.0; - public var dynamicEmitter: Bool = true; - var currentSpeed = 0.0; - var particles: Array; - 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 = r.dynamic_emitter; - var dynValue: Bool = true; - if (dyn != null) { - dynValue = dyn; - } - dynamicEmitter = dynValue; - if (Scene.active.raw.gravity != null) { - gx = Scene.active.raw.gravity[0] * r.weight_gravity; - gy = Scene.active.raw.gravity[1] * r.weight_gravity; - gz = Scene.active.raw.gravity[2] * r.weight_gravity; - } - else { - gx = 0; - gy = 0; - gz = -9.81 * r.weight_gravity; - } - alignx = r.object_align_factor[0]; - aligny = r.object_align_factor[1]; - alignz = r.object_align_factor[2]; - looptime = (r.frame_end - r.frame_start) / frameRate; - lifetime = r.lifetime / frameRate; - animtime = r.loop ? looptime : looptime + lifetime; - spawnRate = ((r.frame_end - r.frame_start) / r.count) / frameRate; - - for (i in 0...r.count) { - particles.push(new Particle(i)); - } - - ready = true; - if (r.auto_start){ - start(); - } - }); - } - - public function start() { - if (r.is_unique) random = Math.random(); - lifetime = r.lifetime / frameRate; - time = 0; - lap = 0; - lapTime = 0; - speed = currentSpeed; - lastSpawnedCount = 0; - instancedData = null; - } - - public function pause() { - speed = 0; - } - - public function resume() { - lifetime = r.lifetime / frameRate; - speed = currentSpeed; - } - - - - // TODO: interrupt smoothly - public function stop() { - end(); - } - - function end() { - lifetime = 0; - speed = 0; - lap = 0; - } - - public function update(object: MeshObject, owner: MeshObject) { - if (!ready || object == null || speed == 0.0) return; - if (iron.App.pauseUpdates) return; - - var prevLap = lap; - - // Copy owner world transform but discard scale - owner.transform.world.decompose(ownerLoc, ownerRot, ownerScl); - if (dynamicEmitter) { - object.transform.loc.x = 0; object.transform.loc.y = 0; object.transform.loc.z = 0; - object.transform.rot = new Quat(); - } else { - object.transform.loc = ownerLoc; - object.transform.rot = ownerRot; - } - - // Set particle size per particle system - object.transform.scale = new Vec4(r.particle_size, r.particle_size, r.particle_size, 1); - - object.transform.buildMatrix(); - owner.transform.buildMatrix(); - object.transform.dim.setFrom(owner.transform.dim); - - dimx = object.transform.dim.x; - dimy = object.transform.dim.y; - - if (object.activeTilesheet != null) { - tilesx = object.activeTilesheet.raw.tilesx; - tilesy = object.activeTilesheet.raw.tilesy; - tilesFramerate = object.activeTilesheet.raw.framerate; - } - - // Animate - time += Time.renderDelta * speed; // realDelta to renderDelta - lap = Std.int(time / animtime); - lapTime = time - lap * animtime; - count = Std.int(lapTime / spawnRate); - - if (lap > prevLap && !r.loop) { - end(); - } - - if (lap > prevLap && r.loop) { - lastSpawnedCount = 0; - } - - updateGpu(object, owner); - } - - public function getData(): Mat4 { - var hair = r.type == 1; - // Store loop flag in the sign: positive -> loop, negative -> no loop - m._00 = r.loop ? animtime : -animtime; - m._01 = hair ? 1 / particles.length : spawnRate; - m._02 = hair ? 1 : lifetime; - m._03 = particles.length; - m._10 = hair ? 0 : alignx; - m._11 = hair ? 0 : aligny; - m._12 = hair ? 0 : alignz; - m._13 = hair ? 0 : r.factor_random; - m._20 = hair ? 0 : gx; - m._21 = hair ? 0 : gy; - m._22 = hair ? 0 : gz; - m._23 = hair ? 0 : r.lifetime_random; - m._30 = tilesx; - m._31 = tilesy; - m._32 = 1 / tilesFramerate; - m._33 = hair ? 1 : lapTime; - return m; - } - - public function getSizeRandom(): FastFloat { - return r.size_random; - } - - public inline function getRandom(): FastFloat { - return random; - } - - public inline function getSize(): FastFloat { - return r.particle_size; - } - - function updateGpu(object: MeshObject, owner: MeshObject) { - if (dynamicEmitter) { - if (!hasUniqueGeom) ensureUniqueGeom(object); - var needSetup = instancedData == null || object.data.geom.instancedVB == null; - if (needSetup) setupGeomGpuDynamic(object, owner); - updateSpawnedInstances(object, owner); - } - else { - if (!hasUniqueGeom) ensureUniqueGeom(object); - if (!object.data.geom.instanced) setupGeomGpu(object, owner); - } - // GPU particles transform is attached to owner object in static mode - } - - function setupGeomGpu(object: MeshObject, owner: MeshObject) { - var instancedData = new Float32Array(particles.length * 3); - var i = 0; - - var normFactor = 1 / 32767; // pa.values are not normalized - var scalePosOwner = owner.data.scalePos; - var scalePosParticle = object.data.scalePos; - var particleSize = r.particle_size; - var scaleFactor = new Vec4().setFrom(owner.transform.scale); - scaleFactor.mult(scalePosOwner / (particleSize * scalePosParticle)); - - switch (r.emit_from) { - case 0: // Vert - var pa = owner.data.geom.positions; - - for (p in particles) { - var j = Std.int(fhash(i) * (pa.values.length / pa.size)); - instancedData.set(i, pa.values[j * pa.size ] * normFactor * scaleFactor.x); i++; - instancedData.set(i, pa.values[j * pa.size + 1] * normFactor * scaleFactor.y); i++; - instancedData.set(i, pa.values[j * pa.size + 2] * normFactor * scaleFactor.z); i++; - } - - case 1: // Face - var positions = owner.data.geom.positions.values; - - for (p in particles) { - // Choose random index array (there is one per material) and random face - var ia = owner.data.geom.indices[Std.random(owner.data.geom.indices.length)]; - var faceIndex = Std.random(Std.int(ia.length / 3)); - - var i0 = ia[faceIndex * 3 + 0]; - var i1 = ia[faceIndex * 3 + 1]; - var i2 = ia[faceIndex * 3 + 2]; - - var v0 = new Vec3(positions[i0 * 4], positions[i0 * 4 + 1], positions[i0 * 4 + 2]); - var v1 = new Vec3(positions[i1 * 4], positions[i1 * 4 + 1], positions[i1 * 4 + 2]); - var v2 = new Vec3(positions[i2 * 4], positions[i2 * 4 + 1], positions[i2 * 4 + 2]); - - var pos = randomPointInTriangle(v0, v1, v2); - - instancedData.set(i, pos.x * normFactor * scaleFactor.x); i++; - instancedData.set(i, pos.y * normFactor * scaleFactor.y); i++; - instancedData.set(i, pos.z * normFactor * scaleFactor.z); i++; - } - - case 2: // Volume - var scaleFactorVolume = new Vec4().setFrom(object.transform.dim); - scaleFactorVolume.mult(0.5 / (particleSize * scalePosParticle)); - - for (p in particles) { - instancedData.set(i, (Math.random() * 2.0 - 1.0) * scaleFactorVolume.x); i++; - instancedData.set(i, (Math.random() * 2.0 - 1.0) * scaleFactorVolume.y); i++; - instancedData.set(i, (Math.random() * 2.0 - 1.0) * scaleFactorVolume.z); i++; - } - } - object.data.geom.setupInstanced(instancedData, 1, Usage.StaticUsage); - } - - // allocate instanced VB once for this object - function setupGeomGpuDynamic(object: MeshObject, owner: MeshObject) { - if (instancedData == null) instancedData = new Float32Array(particles.length * 3); - lastSpawnedCount = 0; - // Create instanced VB once if missing (seed with our instancedData) - if (object.data.geom.instancedVB == null) { - object.data.geom.setupInstanced(instancedData, 1, Usage.DynamicUsage); - } - } - - function ensureUniqueGeom(object: MeshObject) { - if (hasUniqueGeom) return; - var newData: MeshData = null; - new MeshData(object.data.raw, function(dat: MeshData) { - dat.scalePos = object.data.scalePos; - dat.scaleTex = object.data.scaleTex; - dat.format = object.data.format; - newData = dat; - }); - if (newData != null) object.setData(newData); - hasUniqueGeom = true; - } - - function updateSpawnedInstances(object: MeshObject, owner: MeshObject) { - if (instancedData == null) return; - var targetCount = count; - if (targetCount > particles.length) targetCount = particles.length; - if (targetCount <= lastSpawnedCount) return; - - var normFactor = 1 / 32767; - var scalePosOwner = owner.data.scalePos; - var scalePosParticle = object.data.scalePos; - var particleSize = r.particle_size; - var base = 1.0 / (particleSize * scalePosParticle); - - switch (r.emit_from) { - case 0: // Vert - var pa = owner.data.geom.positions; - var osx = owner.transform.scale.x; - var osy = owner.transform.scale.y; - var osz = owner.transform.scale.z; - var pCount = Std.int(pa.values.length / pa.size); - for (idx in lastSpawnedCount...targetCount) { - var j = Std.int(fhash(idx) * pCount); - var lx = pa.values[j * pa.size ] * normFactor * scalePosOwner * osx; - var ly = pa.values[j * pa.size + 1] * normFactor * scalePosOwner * osy; - var lz = pa.values[j * pa.size + 2] * normFactor * scalePosOwner * osz; - tmpV4.x = lx; tmpV4.y = ly; tmpV4.z = lz; tmpV4.w = 1; - tmpV4.applyQuat(ownerRot); - var o = idx * 3; - instancedData.set(o , (tmpV4.x + ownerLoc.x) * base); - instancedData.set(o + 1, (tmpV4.y + ownerLoc.y) * base); - instancedData.set(o + 2, (tmpV4.z + ownerLoc.z) * base); - } - - case 1: // Face - var positions = owner.data.geom.positions.values; - var osx1 = owner.transform.scale.x; - var osy1 = owner.transform.scale.y; - var osz1 = owner.transform.scale.z; - for (idx in lastSpawnedCount...targetCount) { - var ia = owner.data.geom.indices[Std.random(owner.data.geom.indices.length)]; - var faceIndex = Std.random(Std.int(ia.length / 3)); - var i0 = ia[faceIndex * 3 + 0]; - var i1 = ia[faceIndex * 3 + 1]; - var i2 = ia[faceIndex * 3 + 2]; - var v0x = positions[i0 * 4 ], v0y = positions[i0 * 4 + 1], v0z = positions[i0 * 4 + 2]; - var v1x = positions[i1 * 4 ], v1y = positions[i1 * 4 + 1], v1z = positions[i1 * 4 + 2]; - var v2x = positions[i2 * 4 ], v2y = positions[i2 * 4 + 1], v2z = positions[i2 * 4 + 2]; - var rx = Math.random(); var ry = Math.random(); if (rx + ry > 1) { rx = 1 - rx; ry = 1 - ry; } - var pxs = v0x + rx * (v1x - v0x) + ry * (v2x - v0x); - var pys = v0y + rx * (v1y - v0y) + ry * (v2y - v0y); - var pzs = v0z + rx * (v1z - v0z) + ry * (v2z - v0z); - var px = pxs * normFactor * scalePosOwner * osx1; - var py = pys * normFactor * scalePosOwner * osy1; - var pz = pzs * normFactor * scalePosOwner * osz1; - tmpV4.x = px; tmpV4.y = py; tmpV4.z = pz; tmpV4.w = 1; - tmpV4.applyQuat(ownerRot); - var o1 = idx * 3; - instancedData.set(o1 , (tmpV4.x + ownerLoc.x) * base); - instancedData.set(o1 + 1, (tmpV4.y + ownerLoc.y) * base); - instancedData.set(o1 + 2, (tmpV4.z + ownerLoc.z) * base); - } - - case 2: // Volume - var dim = object.transform.dim; - for (idx in lastSpawnedCount...targetCount) { - tmpV4.x = (Math.random() * 2.0 - 1.0) * (dim.x * 0.5); - tmpV4.y = (Math.random() * 2.0 - 1.0) * (dim.y * 0.5); - tmpV4.z = (Math.random() * 2.0 - 1.0) * (dim.z * 0.5); - tmpV4.w = 1; - tmpV4.applyQuat(ownerRot); - var o2 = idx * 3; - instancedData.set(o2 , (tmpV4.x + ownerLoc.x) * base); - instancedData.set(o2 + 1, (tmpV4.y + ownerLoc.y) * base); - instancedData.set(o2 + 2, (tmpV4.z + ownerLoc.z) * base); - } - } - - // Upload full active range [0..targetCount) to this object's instanced VB - var geom = object.data.geom; - if (geom.instancedVB == null) { - geom.setupInstanced(instancedData, 1, Usage.DynamicUsage); - } - var vb = geom.instancedVB.lock(); - var totalFloats = targetCount * 3; // xyz per instance - var i = 0; - while (i < totalFloats) { - vb.setFloat32(i * 4, instancedData[i]); - i++; - } - geom.instancedVB.unlock(); - geom.instanceCount = targetCount; - lastSpawnedCount = targetCount; - } - - inline function fhash(n: Int): Float { - var s = n + 1.0; - s *= 9301.0 % s; - s = (s * 9301.0 + 49297.0) % 233280.0; - return s / 233280.0; -} - - public function remove() {} - - /** - Generates a random point in the triangle with vertex positions abc. - - Please note that the given position vectors are changed in-place by this - function and can be considered garbage afterwards, so make sure to clone - them first if needed. - **/ - public static inline function randomPointInTriangle(a: Vec3, b: Vec3, c: Vec3): Vec3 { - // Generate a random point in a square where (0, 0) <= (x, y) < (1, 1) - var x = Math.random(); - var y = Math.random(); - - if (x + y > 1) { - // We're in the upper right triangle in the square, mirror to lower left - x = 1 - x; - y = 1 - y; - } - - // Transform the point to the triangle abc - var u = b.sub(a); - var v = c.sub(a); - return a.add(u.mult(x).add(v.mult(y))); - } -} - -class Particle { - public var i: Int; - - public var x = 0.0; - public var y = 0.0; - public var z = 0.0; - - public var cameraDistance: Float; - - public function new(i: Int) { - this.i = i; - } -} - +#if lnx_gpu_particles +typedef ParticleSystem = ParticleSystemGPU; +#elseif lnx_cpu_particles +typedef ParticleSystem = ParticleSystemCPU; +#else +class ParticleSystem { public function new() { } } #end diff --git a/leenkx/Sources/iron/object/ParticleSystemCPU.hx b/leenkx/Sources/iron/object/ParticleSystemCPU.hx new file mode 100644 index 00000000..fe251115 --- /dev/null +++ b/leenkx/Sources/iron/object/ParticleSystemCPU.hx @@ -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 = []; + + // Lnx props + var autoStart: Bool = true; // auto_start + var localCoords: Bool = false; // local_coords + var loop: Bool = false; // loop + + // Internal logic + var owner: MeshObject; + var lifetimeSeconds: FastFloat = 0.0; + var spawnRate: FastFloat = 0.0; + var spawnFactor: Int = 1; + var spawnedParticles: Int = 0; + var particleScale: FastFloat = 1.0; + var loopAnim: TAnim; + var spawnTime: FastFloat = 0; + + var randQuat: Quat; + var phaseQuat: Quat; + + // Tween scaling + var scaleElementsCount: Int = 0; + var scaleRampSizeFactor: FastFloat = 0; + var rampPositions: Array = []; + var rampColors: Array = []; + + // Optimization + var particlePool: Array = []; + var particlePhysics: Map = []; + + public function new(sceneName: String, pref: TParticleReference, mo: MeshObject) { + Data.getParticle(sceneName, pref.particle, function (b: ParticleData) { + data = b; + r = data.raw; + owner = mo; + + frameRate = r.fps; + type = r.type; + count = r.count; + frameStart = r.frame_start; + frameEnd = r.frame_end; + lifetime = r.lifetime; + lifetimeRandom = r.lifetime_random; + emitFrom = r.emit_from; + + rotation = r.use_rotations; + orientationAxis = r.rotation_mode; + rotationRandom = r.rotation_factor_random; + phase = r.phase_factor; + phaseRandom = r.phase_factor_random; + dynamicRotation = r.use_dynamic_rotation; + + instanceObject = r.instance_object; + + scale = r.particle_size; + scaleRandom = r.size_random; + + velocity = new Vec3(r.object_align_factor[0], r.object_align_factor[1], r.object_align_factor[2]).mult(frameRate / baseFrameRate).mult(1 / scale); + velocityRandom = r.factor_random * (frameRate / baseFrameRate); + + if (Scene.active.raw.gravity != null) { + gravity = new Vec3(Scene.active.raw.gravity[0], Scene.active.raw.gravity[1], Scene.active.raw.gravity[2]).mult(frameRate / baseFrameRate).mult(1 / scale); + } + gravityFactor = r.weight_gravity * (frameRate / baseFrameRate); + textureFactor = r.weight_texture; + + if (r.texture_slots != null) { + for (slot in Reflect.fields(r.texture_slots)) { + textureSlots[slot] = Reflect.field(r.texture_slots, slot); + } + } + + autoStart = r.auto_start; + localCoords = r.local_coords; + loop = r.loop; + + spawnRate = ((frameEnd - frameStart) / count) / frameRate; + lifetimeSeconds = lifetime / frameRate; + + scaleElementsCount = getRampElementsLength(); + scaleRampSizeFactor = getRampSizeFactor(); + + Scene.active.notifyOnInit(function () { + var i: Int; + for (i in 0...count) addToPool(); + }); + + switch (type) { + case 0: // Emission + loopAnim = { + tick: function () { + spawnTime += Time.delta * Time.scale; + var expected: Int = Math.floor(spawnTime / spawnRate); + while (spawnedParticles < expected && spawnedParticles < count) { + spawnParticle(); + spawnedParticles++; + } + updateParticles(); + }, + target: null, + props: null, + duration: loop ? lifetimeSeconds : lifetimeSeconds * 2, + done: function () { + if (loop) start(); + } + } + case 1: // Hair + Scene.active.notifyOnInit(function () { + var i: Int; + for (i in 0...count) spawnParticle(); + }); + default: + } + + Scene.active.notifyOnInit(function () { + if (autoStart) start(); + }); + }); + } + + public function start() { + if (type != 0) return; + spawnTime = 0; + spawnedParticles = 0; + Tween.to(loopAnim); + } + + // TODO + public function pause() { + + } + + // TODO + public function resume() { + + } + + public function stop() { + if (type != 0) return; + spawnTime = 0; + spawnedParticles = 0; + Tween.stop(loopAnim); + + for (particle => physics in particlePhysics) releaseParticle(particle); + particlePhysics.clear(); + } + + function addToPool() { + Scene.active.spawnObject(instanceObject, localCoords ? owner : null, function (o: Object) { + o.visible = false; + particlePool.push(o); + }); + } + + function getFreeParticle(): Object { + for (particle in particlePool) { + if (!particle.visible) { + particle.visible = true; + return particle; + } + } + return null; + } + + function releaseParticle(o: Object) { + o.visible = false; + o.transform.loc = new Vec4(); + o.transform.rot = new Quat(); + o.transform.scale = new Vec4(1, 1, 1, 1); + } + + function spawnParticle() { + var o: Object = getFreeParticle(); + if (o == null) { + addToPool(); + o = getFreeParticle(); + } + + owner.transform.buildMatrix(); + + var objectPos: Vec4 = new Vec4(); + var objectRot: Quat = new Quat(); + var objectScale: Vec4 = new Vec4(); + owner.transform.world.decompose(objectPos, objectRot, objectScale); + + o.visible = true; + + var normFactor: FastFloat = 1 / 32767; + var scalePos: FastFloat = owner.data.scalePos; + var scalePosParticle: FastFloat = cast(o, MeshObject).data.scalePos; + + // TODO: add all properties from Blender's UI + switch (emitFrom) { + case 0: // Vertices + var pa: TVertexArray = owner.data.geom.positions; + var i: Int = Std.int(Math.random() * (pa.values.length / pa.size)); + var loc: Vec4 = new Vec4(pa.values[i * pa.size] * normFactor, pa.values[i * pa.size + 1] * normFactor, pa.values[i * pa.size + 2] * normFactor, 1); + + if (!localCoords) { + loc.applyQuat(objectRot); + loc.add(objectPos); + } + o.transform.loc.setFrom(loc); + case 1: // Faces + var positions: Int16Array = owner.data.geom.positions.values; + var ia: Uint32Array = owner.data.geom.indices[Std.random(owner.data.geom.indices.length)]; + var faceIndex: Int = Std.random(Std.int(ia.length / 3)); + + var i0 = ia[faceIndex * 3 + 0]; + var i1 = ia[faceIndex * 3 + 1]; + var i2 = ia[faceIndex * 3 + 2]; + + var v0: Vec3 = new Vec3(positions[i0 * 4], positions[i0 * 4 + 1], positions[i0 * 4 + 2]); + var v1: Vec3 = new Vec3(positions[i1 * 4], positions[i1 * 4 + 1], positions[i1 * 4 + 2]); + var v2: Vec3 = new Vec3(positions[i2 * 4], positions[i2 * 4 + 1], positions[i2 * 4 + 2]); + + var pos: Vec3 = randomPointInTriangle(v0, v1, v2); + + var loc: Vec4 = new Vec4(pos.x, pos.y, pos.z, 1).mult(normFactor); + if (!localCoords) { + loc.applyQuat(objectRot); + loc.add(objectPos); + } + + o.transform.loc.setFrom(loc); + case 2: // Volume + var scaleFactorVolume: Vec4 = new Vec4().setFrom(owner.transform.dim); + scaleFactorVolume.mult(0.5); + var loc: Vec4 = new Vec4((Math.random() * 2.0 - 1.0) * scaleFactorVolume.x, (Math.random() * 2.0 - 1.0) * scaleFactorVolume.y, (Math.random() * 2.0 - 1.0) * scaleFactorVolume.z, 1); + + if (!localCoords) { + loc.applyQuat(objectRot); + loc.add(objectPos); + } + o.transform.loc.setFrom(loc); + } + + particleScale = 1 - scaleRandom * Math.random(); + var localFactor: Vec3 = localCoords ? new Vec3(objectScale.x, objectScale.y, objectScale.z) : new Vec3(1, 1, 1); + var sc: Vec4 = new Vec4(o.transform.scale.x / localFactor.x, o.transform.scale.y / localFactor.y, o.transform.scale.z / localFactor.z, 1.0).mult(scale).mult(particleScale); + var randomLifetime: FastFloat = lifetimeSeconds * (1 - Math.random() * lifetimeRandom); + + if (scaleElementsCount != 0) { + rampPositions = getRampPositions(); + rampColors = getRampColors(); + } else { + o.transform.scale.setFrom(sc); + } + o.transform.buildMatrix(); + + var randomX: FastFloat = (Math.random() * 2 / (scale * particleScale) - 1 / (scale * particleScale)) * velocityRandom; + var randomY: FastFloat = (Math.random() * 2 / (scale * particleScale) - 1 / (scale * particleScale)) * velocityRandom; + var randomZ: FastFloat = (Math.random() * 2 / (scale * particleScale) - 1 / (scale * particleScale)) * velocityRandom; + var g: Vec3 = new Vec3(); + + var rotatedVelocity: Vec4 = new Vec4(velocity.x + randomX, velocity.y + randomY, velocity.z + randomZ, 1); + if (!localCoords) rotatedVelocity.applyQuat(objectRot); + + if (rotation) { + // Rotation phase and randomness. Wrap values between -1 and 1. + randQuat = new Quat().fromEuler((Math.random() * 2 - 1) * Math.PI * rotationRandom, (Math.random() * 2 - 1) * Math.PI * rotationRandom, (Math.random() * 2 - 1) * Math.PI * rotationRandom); + var phaseRand: FastFloat = (Math.random() * 2 - 1) * phaseRandom; + var phaseValue: FastFloat = phase + phaseRand; + while (phaseValue > 1) phaseValue -= 2; + while (phaseValue < -1) phaseValue += 2; + var dirQuat: Quat = new Quat(); + phaseQuat = new Quat().fromEuler(0, phaseValue * Math.PI, 0); + + switch (orientationAxis) { + case 0: // None + o.transform.rotate(new Vec4(0, 0, 1, 1), -Math.PI * 0.5); + case 1: // Normal + case 2: // Normal-Tangent + case 3: // Velocity/Hair + setVelocityHair(o, rotatedVelocity, randQuat, phaseQuat); + case 4: // Global X + o.transform.rot.fromEuler(0, 0, -Math.PI * 0.5).mult(phaseQuat).mult(randQuat); + case 5: // Global Y + o.transform.rot.fromEuler(0, 0, 0).mult(phaseQuat).mult(randQuat); + case 6: // Global Z + o.transform.rot.fromEuler(0, -Math.PI * 0.5, -Math.PI * 0.5).mult(phaseQuat).mult(randQuat); + case 7: // Object X + o.transform.rot.setFrom(objectRot); + dirQuat.fromEuler(0, 0, -Math.PI * 0.5); + o.transform.rot.mult(dirQuat).mult(phaseQuat).mult(randQuat); + case 8: // Object Y + o.transform.rot.setFrom(objectRot); + o.transform.rot.mult(phaseQuat).mult(randQuat); + case 9: // Object Z + o.transform.rot.setFrom(objectRot); + dirQuat.fromEuler(0, -Math.PI * 0.5, 0).mult(new Quat().fromEuler(0, 0, -Math.PI * 0.5)); + o.transform.rot.mult(dirQuat).mult(phaseQuat).mult(randQuat); + default: + } + } else { + o.transform.rotate(new Vec4(0, 0, 1, 1), -Math.PI * 0.5); + } + + var physics: TParticlePhysics = { + velocity: rotatedVelocity.clone(), + gravity: gravity.clone().mult(gravityFactor), + lifetime: randomLifetime, + age: 0.0, + hasScaleRamp: scaleElementsCount != 0, + baseScale: sc.clone(), + rampPositions: rampPositions.copy(), + rampColors: rampColors.copy(), + scaleRampSizeFactor: scaleRampSizeFactor + }; + + particlePhysics.set(o, physics); + o.transform.buildMatrix(); + } + } + + function setVelocityHair(object: Object, velocity: Vec4, randQuat: Quat, phaseQuat: Quat) { + var dir: Vec4 = velocity.clone().normalize(); + var yaw: FastFloat = Math.atan2(-dir.x, dir.y); + var pitch: FastFloat = Math.asin(dir.z); + var targetRot: Quat = new Quat().fromEuler(pitch, 0, yaw); + + targetRot.mult(randQuat); + object.transform.rot.setFrom(targetRot.mult(phaseQuat)); + } + + function updateParticles() { + for (particle => physics in particlePhysics) { + physics.age += Time.delta * Time.scale; + + if (physics.age >= physics.lifetime) { + particlePhysics.remove(particle); + releaseParticle(particle); + continue; + } + + physics.velocity.x += physics.gravity.x * Time.delta * Time.scale; + physics.velocity.y += physics.gravity.y * Time.delta * Time.scale; + physics.velocity.z += physics.gravity.z * Time.delta * Time.scale; + + particle.transform.translate( + physics.velocity.x * Time.delta * Time.scale, + physics.velocity.y * Time.delta * Time.scale, + physics.velocity.z * Time.delta * Time.scale + ); + + if (rotation && dynamicRotation && orientationAxis == 3) setVelocityHair(particle, physics.velocity, randQuat, phaseQuat); + + if (physics.hasScaleRamp && physics.rampPositions.length > 1) { + var normalizedAge: FastFloat = physics.age / physics.lifetime; + var scaleMultiplier: FastFloat = interpolateRampValue(normalizedAge, physics.rampPositions, physics.rampColors); + var finalScale: FastFloat = scale * (particleScale * (1 - physics.scaleRampSizeFactor) + scaleMultiplier * physics.scaleRampSizeFactor); + particle.transform.scale.setFrom(physics.baseScale.clone().mult(finalScale)); + } + + particle.transform.buildMatrix(); + } + } + + // Linear interpolation + function interpolateRampValue(normalizedAge: FastFloat, positions: Array, colors: Array): FastFloat { + if (positions.length == 0) return 1.0; + if (normalizedAge <= positions[0]) return colors[0]; + if (normalizedAge >= positions[positions.length - 1]) return colors[colors.length - 1]; + + var i: Int; + for (i in 0...(positions.length - 1)) { + if (normalizedAge >= positions[i] && normalizedAge <= positions[i + 1]) { + var t: FastFloat = (normalizedAge - positions[i]) / (positions[i + 1] - positions[i]); + return colors[i] + t * (colors[i + 1] - colors[i]); + } + } + + return colors[colors.length - 1]; + } + + function getRampSizeFactor(): FastFloat { + // Just using the first slot for now: 1 texture slot + // TODO: use all available slots ? + for (slot in textureSlots.keys()) { + var s: Dynamic = textureSlots[slot]; + if (s != null && s.use_map_size) { + var sizeFactor: FastFloat = s.size_factor; + return sizeFactor * textureFactor; + } + } + return 0.0; + } + + function getRampElementsLength(): Int { + for (slot in textureSlots.keys()) { + var s: Dynamic = textureSlots[slot]; + if (s == null) continue; + var tex: Dynamic = s.texture; + if (tex == null) continue; + if (tex.use_color_ramp) { + var ramp: Dynamic = tex.color_ramp; + if (ramp == null) continue; + var elems: Dynamic = ramp.elements; + if (elems == null) continue; + return elems.length; + } + } + return 0; + } + + function getRampPositions(): Array { + // Just using the first slot for now: 1 texture slot + // TODO: use all available slots ? + for (slot in textureSlots.keys()) { + var s: Dynamic = textureSlots[slot]; + if (s == null) continue; + var tex: Dynamic = s.texture; + if (tex == null) continue; + if (tex.use_color_ramp) { + var ramp: Dynamic = tex.color_ramp; + if (ramp == null) continue; + var elems: Dynamic = ramp.elements; + if (elems == null) continue; + var positions: Array = []; + for (i in 0...elems.length) { + positions.push(elems[i].position); + } + return positions; + } + } + return []; + } + + function getRampColors(): Array { + // Just using the first slot for now: 1 texture slot + // TODO: use all available slots ? + for (slot in textureSlots.keys()) { + var s: Dynamic = textureSlots[slot]; + if (s == null) continue; + var tex: Dynamic = s.texture; + if (tex == null) continue; + if (tex.use_color_ramp) { + var ramp: Dynamic = tex.color_ramp; + if (ramp == null) continue; + var elems: Dynamic = ramp.elements; + if (elems == null) continue; + var colors: Array = []; + for (i in 0...elems.length) { + colors.push(elems[i].color.b); // Just need R, G or B for black and white images. Using B as it can be interpreted as V with HSV + } + return colors; + } + } + return []; + } + + public function remove() { + for (particle in particlePool) particle.remove(); + } + + /** + Generates a random point in the triangle with vertex positions abc. + + Please note that the given position vectors are changed in-place by this + function and can be considered garbage afterwards, so make sure to clone + them first if needed. + **/ + public static inline function randomPointInTriangle(a: Vec3, b: Vec3, c: Vec3): Vec3 { + // Generate a random point in a square where (0, 0) <= (x, y) < (1, 1) + var x = Math.random(); + var y = Math.random(); + + if (x + y > 1) { + // We're in the upper right triangle in the square, mirror to lower left + x = 1 - x; + y = 1 - y; + } + + // Transform the point to the triangle abc + var u = b.sub(a); + var v = c.sub(a); + return a.add(u.mult(x).add(v.mult(y))); + } + + typedef TParticlePhysics = { + var velocity: Vec4; + var gravity: Vec3; + var lifetime: Float; + var age: Float; + var hasScaleRamp: Bool; + var baseScale: Vec4; + var rampPositions: Array; + var rampColors: Array; + var scaleRampSizeFactor: FastFloat; + } +} +#end diff --git a/leenkx/Sources/iron/object/ParticleSystemGPU.hx b/leenkx/Sources/iron/object/ParticleSystemGPU.hx new file mode 100644 index 00000000..94b9b66e --- /dev/null +++ b/leenkx/Sources/iron/object/ParticleSystemGPU.hx @@ -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; + 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 diff --git a/leenkx/Sources/iron/object/Tilesheet.hx b/leenkx/Sources/iron/object/Tilesheet.hx index 86328e0c..d3ab97d7 100644 --- a/leenkx/Sources/iron/object/Tilesheet.hx +++ b/leenkx/Sources/iron/object/Tilesheet.hx @@ -1,53 +1,258 @@ package iron.object; -import iron.Scene; -import iron.data.Data; +import iron.App; import iron.data.SceneFormat; import iron.system.Time; +import haxe.ds.Map; + -@:allow(iron.Scene) class Tilesheet { - public var tileX = 0.0; // Tile offset on tilesheet texture 0-1 - public var tileY = 0.0; - - public var raw: TTilesheetData; + public var tileX: Float = 0.0; + public var tileY: Float = 0.0; + public var flipX: Bool = false; + public var flipY: Bool = false; + public var paused: Bool = false; + public var frame: Int = 0; + public var actions: Array; public var action: TTilesheetAction = null; - var ready: Bool; - public var paused = false; - public var frame = 0; - var time = 0.0; + public var ready: Bool = false; + var time: Float = 0.0; var onActionComplete: Void->Void = null; + var onReady: Void->Void = null; + var onEvent: String->Void = null; // Callback for tilesheet events + var prevFrame: Int = -1; // Track previous frame to detect changes + var owner: MeshObject = null; + var currentMesh: MeshObject = null; + var meshCache: Map = new Map(); + var pendingAction: String = null; + var pendingOnComplete: Void->Void = null; - public function new(sceneName: String, tilesheet_ref: String, tilesheet_action_ref: String) { - ready = false; - Data.getSceneRaw(sceneName, function(format: TSceneFormat) { - for (ts in format.tilesheet_datas) { - if (ts.name == tilesheet_ref) { - raw = ts; - Scene.active.tilesheets.push(this); - play(tilesheet_action_ref); - ready = true; - break; + public function new(tilesheetData: TTilesheetData, ownerObject: MeshObject = null) { + owner = ownerObject; + actions = tilesheetData.actions; + + pendingAction = tilesheetData.start_action; + if ((pendingAction == null || pendingAction == "") && actions.length > 0) { + pendingAction = actions[0].name; + } + + flipX = tilesheetData.flipx; + flipY = tilesheetData.flipy; + + // If no actions need mesh swapping, ready immediately + var hasMeshActions: Bool = false; + for (a in actions) { + if (a.mesh != null && a.mesh != "") { + hasMeshActions = true; + break; + } + } + + if (!hasMeshActions) { + ready = true; + if (pendingAction != null) { + playAction(pendingAction); + pendingAction = null; + } + if (onReady != null) onReady(); + } + } + + public function update() { + if (App.pauseUpdates) return; + + if (!ready) { + if (tryInitialize()) { + ready = true; + if (pendingAction != null) { + playAction(pendingAction, pendingOnComplete); + pendingAction = null; + pendingOnComplete = null; + } + if (onReady != null) onReady(); + } + return; + } + + if (paused || action == null || action.start >= action.end) return; + + time += Time.renderDelta; + + var frameTime = 1 / action.framerate; + var framesToAdvance = 0; + + while (time >= frameTime) { + time -= frameTime; + framesToAdvance++; + } + + if (framesToAdvance > 0) { + setFrame(frame + framesToAdvance); + } + } + + function tryInitialize(): Bool { + if (owner == null) return false; + + // If no children, use the owner mesh itself + if (owner.children == null || owner.children.length == 0) { + if (owner.data != null && !meshCache.exists(owner.data.name)) { + meshCache.set(owner.data.name, owner); + // Also cache by object name for flexible lookup + if (owner.name != owner.data.name) { + meshCache.set(owner.name, owner); } } - }); + } else { + // Use child meshes for mesh swapping + for (child in owner.children) { + if (Std.isOfType(child, MeshObject)) { + var meshChild = cast(child, MeshObject); + if (meshChild.data != null && !meshCache.exists(meshChild.data.name)) { + meshCache.set(meshChild.data.name, meshChild); + meshChild.visible = false; + // Also cache by object name for flexible lookup + if (meshChild.name != meshChild.data.name) { + meshCache.set(meshChild.name, meshChild); + } + } + } + } + } + + for (a in actions) { + if (a.mesh != null && a.mesh != "" && !meshCache.exists(a.mesh)) { + if (findMatchingMesh(a.mesh) == null) return false; + } + } + return true; + } + + /** Find mesh by base name pattern (handles linked objects with different suffixes). */ + function findMatchingMesh(actionMeshName: String): MeshObject { + var baseName = actionMeshName; + + // Strip "Mesh" prefix if present + if (StringTools.startsWith(baseName, "Mesh")) { + baseName = baseName.substr(4); + } + + // Strip suffix after underscore (e.g., "_character.blend") + var idx = baseName.indexOf("_"); + if (idx > 0) baseName = baseName.substr(0, idx); + + for (meshName in meshCache.keys()) { + if (meshName.indexOf(baseName) != -1) { + var mesh = meshCache.get(meshName); + meshCache.set(actionMeshName, mesh); // Cache alias + return mesh; + } + } + return null; } public function play(action_ref: String, onActionComplete: Void->Void = null) { - this.onActionComplete = onActionComplete; - for (a in raw.actions) { + if (actions == null) return; + + if (!ready) { + pendingAction = action_ref; + pendingOnComplete = onActionComplete; + return; + } + + playAction(action_ref, onActionComplete); + } + + public function notifyOnReady(callback: Void->Void) { + onReady = callback; + if (ready) onReady(); + } + + public function notifyOnEvent(callback: String->Void) { + onEvent = callback; + } + + function playAction(action_ref: String, onComplete: Void->Void = null) { + if (action != null && action.name == action_ref) { + paused = false; + return; + } + + onActionComplete = onComplete; + + for (a in actions) { if (a.name == action_ref) { action = a; break; } } + + if (action == null) return; + + if (action.mesh != null && action.mesh != "") { + var targetMesh = meshCache.get(action.mesh); + if (targetMesh != null && targetMesh != currentMesh) { + swapMesh(targetMesh); + } + } + + prevFrame = -1; // Reset previous frame for new action setFrame(action.start); paused = false; time = 0.0; } + function swapMesh(meshObj: MeshObject) { + if (owner == null || meshObj == null) return; + currentMesh = meshObj; + if (meshObj.data != null) owner.setData(meshObj.data); + if (meshObj.materials != null) owner.materials = meshObj.materials; + } + + function setFrame(f: Int) { + frame = f; + + if (frame > action.end && action.start < action.end) { + // Check for events on last frame before completing + checkEvents(prevFrame, action.end); + if (onActionComplete != null) onActionComplete(); + if (action.loop) { + prevFrame = -1; // Reset for loop + setFrame(action.start); + } else { + paused = true; + } + return; + } + + // Check for events between previous frame and current frame + checkEvents(prevFrame, frame); + prevFrame = frame; + + var tx = frame % action.tilesx; + var ty = Std.int(frame / action.tilesx); + tileX = tx / action.tilesx; + tileY = ty / action.tilesy; + } + + /** Check and fire events for frames between fromFrame (exclusive) and toFrame (inclusive). */ + function checkEvents(fromFrame: Int, toFrame: Int) { + if (onEvent == null || action == null || action.events == null) return; + + // Convert to action-relative frame numbers + var relativeFrom = fromFrame - action.start; + var relativeTo = toFrame - action.start; + + for (evt in action.events) { + // Fire event if it falls in the range (fromFrame, toFrame] + if (evt.frame > relativeFrom && evt.frame <= relativeTo) { + onEvent(evt.name); + } + } + } + public function pause() { paused = true; } @@ -57,61 +262,33 @@ class Tilesheet { } public function remove() { - Scene.active.tilesheets.remove(this); + ready = false; + action = null; + actions = null; + owner = null; + currentMesh = null; + pendingAction = null; + pendingOnComplete = null; + onEvent = null; + prevFrame = -1; + meshCache.clear(); } - /** - * Set the frame of the current active tilesheet action. Automatically un-pauses action. - * @param frame Frame offset with 0 as the first frame of the active action. - **/ public function setFrameOffset(frame: Int) { + if (action == null) return; setFrame(action.start + frame); paused = false; } - /** - * Returns the current frame. - * @return Frame offset with 0 as the first frame of the active action. - */ public function getFrameOffset(): Int { - return frame - action.start; + return action != null ? frame - action.start : 0; } - function update() { - if (!ready || paused || action.start >= action.end) return; - - time += Time.renderDelta; - - var frameTime = 1 / raw.framerate; - var framesToAdvance = 0; - - // Check how many animation frames passed during the last render frame - // and catch up if required. The remaining `time` that couldn't fit in - // another animation frame will be used in the next `update()`. - while (time >= frameTime) { - time -= frameTime; - framesToAdvance++; - } - - if (framesToAdvance != 0) { - setFrame(frame + framesToAdvance); - } + public function getTilesX(): Int { + return action != null ? action.tilesx : 1; } - function setFrame(f: Int) { - frame = f; - - // Action end - if (frame > action.end && action.start < action.end) { - if (onActionComplete != null) onActionComplete(); - if (action.loop) setFrame(action.start); - else paused = true; - return; - } - - var tx = frame % raw.tilesx; - var ty = Std.int(frame / raw.tilesx); - tileX = tx * (1 / raw.tilesx); - tileY = ty * (1 / raw.tilesy); + public function getTilesY(): Int { + return action != null ? action.tilesy : 1; } } diff --git a/leenkx/Sources/iron/object/Transform.hx b/leenkx/Sources/iron/object/Transform.hx index b214e201..b2162830 100644 --- a/leenkx/Sources/iron/object/Transform.hx +++ b/leenkx/Sources/iron/object/Transform.hx @@ -286,7 +286,7 @@ class Transform { public function applyParentInverse() { var pt = object.parent.transform; pt.buildMatrix(); - temp.getInverse(pt.world); + temp.getInverse(pt.local); this.local.multmat(temp); this.decompose(); this.buildMatrix(); @@ -295,7 +295,7 @@ class Transform { public function applyParent() { var pt = object.parent.transform; pt.buildMatrix(); - this.local.multmat(pt.world); + this.local.multmat(pt.local); this.decompose(); this.buildMatrix(); } diff --git a/leenkx/Sources/iron/object/Uniforms.hx b/leenkx/Sources/iron/object/Uniforms.hx index e307092f..0ec11bf9 100644 --- a/leenkx/Sources/iron/object/Uniforms.hx +++ b/leenkx/Sources/iron/object/Uniforms.hx @@ -681,7 +681,11 @@ class Uniforms { } #end case "_backgroundCol": { - if (camera.data.raw.clear_color != null) helpVec.set(camera.data.raw.clear_color[0], camera.data.raw.clear_color[1], camera.data.raw.clear_color[2]); + if (Scene.active.world != null) { + var col = Scene.active.world.raw.background_color; + helpVec.set(((col >> 16) & 0xff) / 255, ((col >> 8) & 0xff) / 255, (col & 0xff) / 255); + } + else if (camera.data.raw.clear_color != null) helpVec.set(camera.data.raw.clear_color[0], camera.data.raw.clear_color[1], camera.data.raw.clear_color[2]); v = helpVec; } case "_hosekSunDirection": { @@ -1095,7 +1099,7 @@ class Uniforms { m = helpMat; } #end - #if lnx_particles + #if lnx_gpu_particles case "_particleData": { var mo = cast(object, MeshObject); if (mo.particleOwner != null && mo.particleOwner.particleSystems != null) { @@ -1106,18 +1110,9 @@ class Uniforms { } if (m == null) { - #if lnx_spot - if (c.link.startsWith("_biasLightWorldViewProjectionMatrixSpot")) { - var light = getSpot(c.link.charCodeAt(c.link.length - 1) - "0".code); - if (light != null) { - object == null ? helpMat.setIdentity() : helpMat.setFrom(object.transform.worldUnpack); - helpMat.multmat(light.VP); - helpMat.multmat(biasMat); - m = helpMat; - } - } + #if (!lnx_clusters && lnx_spot) if (c.link.startsWith("_biasLightViewProjectionMatrixSpot")) { - var light = getSpot(c.link.charCodeAt(c.link.length - 1) - "0".code); + var light = getSpot(0); if (light != null) { helpMat.setFrom(light.VP); helpMat.multmat(biasMat); @@ -1251,14 +1246,19 @@ class Uniforms { var vy: Null = null; switch (c.link) { case "_tilesheetOffset": { - var ts = cast(object, MeshObject).activeTilesheet; + var ts = cast(object, MeshObject).tilesheet; vx = ts.tileX; vy = ts.tileY; } + case "_tilesheetFlip": { + var ts = cast(object, MeshObject).tilesheet; + vx = ts.flipX ? 1.0 : 0.0; + vy = ts.flipY ? 1.0 : 0.0; + } case "_tilesheetTiles": { - var ts = cast(object, MeshObject).activeTilesheet; - vx = ts.raw.tilesx; - vy = ts.raw.tilesy; + var ts = cast(object, MeshObject).tilesheet; + vx = ts.getTilesX(); + vy = ts.getTilesY(); } #if lnx_morph_target case "_morphScaleOffset": { @@ -1306,7 +1306,7 @@ class Uniforms { case "_texUnpack": { f = texUnpack != null ? texUnpack : 1.0; } - #if lnx_particles + #if lnx_gpu_particles case "_particleSizeRandom": { var mo = cast(object, MeshObject); if (mo.particleOwner != null && mo.particleOwner.particleSystems != null) { diff --git a/leenkx/Sources/iron/system/Input.hx b/leenkx/Sources/iron/system/Input.hx index 62bd880d..936236f5 100644 --- a/leenkx/Sources/iron/system/Input.hx +++ b/leenkx/Sources/iron/system/Input.hx @@ -601,8 +601,10 @@ class Keyboard extends VirtualInput { function downListener(code: KeyCode) { var s = keyCode(code); - keysFrame.push(s); - keysStarted.set(s, true); + if (!keysDown.get(s)) { + keysFrame.push(s); + keysStarted.set(s, true); + } keysDown.set(s, true); repeatTime = kha.Scheduler.time() + 0.4; @@ -618,8 +620,10 @@ class Keyboard extends VirtualInput { function upListener(code: KeyCode) { var s = keyCode(code); - keysFrame.push(s); - keysReleased.set(s, true); + if (keysDown.get(s)) { + keysFrame.push(s); + keysReleased.set(s, true); + } keysDown.set(s, false); #if kha_android_rmb @@ -746,7 +750,11 @@ class Gamepad extends VirtualInput { } else if (axis == 1 || axis == 3) { // Y stick.lastY = stick.y; + #if (kha_html5 || lnx_debug_html5) + stick.y = -value; + #else stick.y = value; + #end stick.movementY = stick.y - stick.lastY; } stick.moved = true; @@ -765,13 +773,12 @@ class Gamepad extends VirtualInput { } class Sensor { - public var x = 0.0; public var y = 0.0; public var z = 0.0; - public function new() { - kha.input.Sensor.get(kha.input.SensorType.Accelerometer).notify(listener); + public function new(sensorType: kha.input.SensorType = kha.input.SensorType.Accelerometer) { + kha.input.Sensor.get(sensorType).notify(listener); } function listener(x: Float, y: Float, z: Float) { diff --git a/leenkx/Sources/iron/system/LnxPack.hx b/leenkx/Sources/iron/system/LnxPack.hx index 32d4668f..533ccec6 100644 --- a/leenkx/Sources/iron/system/LnxPack.hx +++ b/leenkx/Sources/iron/system/LnxPack.hx @@ -111,12 +111,18 @@ class LnxPack { #if js var out = {}; #else - var out = Type.createEmptyInstance(getClass(key, parentKey)); + var cls = getClass(key, parentKey); + var out: Dynamic = cls != null ? Type.createEmptyInstance(cls) : {}; + var fields: Array = cls != null ? Type.getInstanceFields(cls) : null; #end for (n in 0...length) { - var k = Std.string(read(i)); + var raw = read(i); + var k = Std.string(raw); var v = read(i, k, key); - Reflect.setField(out, k, v); + #if !js + if (fields == null || fields.indexOf(k) != -1) + #end + Reflect.setField(out, k, v); } return out; } @@ -161,7 +167,9 @@ class LnxPack { case "tracks": TTrack; case "morph_target": TMorphTarget; case "vertex_groups": TVertex_groups; - case _: TSceneFormat; + case "tilesheet": TTilesheetData; + case "events": TTilesheetEvent; + case _: null; } } #end diff --git a/leenkx/Sources/iron/system/Time.hx b/leenkx/Sources/iron/system/Time.hx index 53f8ceba..ba502ca2 100644 --- a/leenkx/Sources/iron/system/Time.hx +++ b/leenkx/Sources/iron/system/Time.hx @@ -2,27 +2,19 @@ package iron.system; class Time { public static var scale = 1.0; - - // TODO: VR Frame Time Override - used to sync physics with VR headset refresh rate - #if lnx_vr - public static var vrFrameTime: Float = -1.0; // VR frame time in seconds (-1 = not in VR) - static var lastVRFrameTime: Float = 0.0; - static var vrFrameCount: Int = 0; - static var normalModeLogged: Bool = false; - #end - + static var frequency: Null = null; static function initFrequency() { frequency = kha.Display.primary != null ? kha.Display.primary.frequency : 60; } - + public static var step(get, never): Float; static function get_step(): Float { if (frequency == null) initFrequency(); return 1 / frequency; } - - static var _fixedStep: Null = 1/60; + + static var _fixedStep: Null = 1 / 60; public static var fixedStep(get, never): Float; static function get_fixedStep(): Float { return _fixedStep; @@ -32,31 +24,45 @@ class Time { _fixedStep = value; } + static var _fixedStepInterpolation: Float = 0.0; + public static var fixedStepInterpolation(get, never): Float; + static function get_fixedStepInterpolation(): Float { + return _fixedStepInterpolation; + } + + // TODO: VR Frame Time Override - used to sync physics with VR headset refresh rate + #if lnx_vr + public static var vrFrameTime: Float = -1.0; // VR frame time in seconds (-1 = not in VR) + static var lastVRFrameTime: Float = 0.0; + static var vrFrameCount: Int = 0; + static var normalModeLogged: Bool = false; + #end + static var lastTime = 0.0; static var _delta = 0.0; public static var delta(get, never): Float; static function get_delta(): Float { return _delta; } - + static var lastRenderTime = 0.0; static var _renderDelta = 0.0; public static var renderDelta(get, never): Float; static function get_renderDelta(): Float { return _renderDelta; } - + public static inline function time(): Float { - return kha.Scheduler.time() * scale; + return kha.Scheduler.time(); } - + public static inline function realTime(): Float { - return kha.Scheduler.realTime() * scale; + return kha.Scheduler.realTime(); } public static function update() { #if lnx_vr - // TODO: use VR frame time when in VR present mode to sync physics with headset refresh + // TODO: use VR frame time when in VR present mode to sync physics with VR headset refresh if (vrFrameTime >= 0.0) { if (lastVRFrameTime > 0.0) { _delta = vrFrameTime - lastVRFrameTime; diff --git a/leenkx/Sources/iron/system/Tween.hx b/leenkx/Sources/iron/system/Tween.hx index e0c11edc..0dcacdd5 100644 --- a/leenkx/Sources/iron/system/Tween.hx +++ b/leenkx/Sources/iron/system/Tween.hx @@ -255,7 +255,7 @@ typedef TAnim = { @:optional var _normalize: Array; } -@:enum abstract Ease(Int) from Int to Int { +enum abstract Ease(Int) from Int to Int { var Linear = 0; var SineIn = 1; var SineOut = 2; diff --git a/leenkx/Sources/leenkx/logicnode/AddParticleToObjectNode.hx b/leenkx/Sources/leenkx/logicnode/AddParticleToObjectNode.hx index 5bcd6b04..456a27aa 100644 --- a/leenkx/Sources/leenkx/logicnode/AddParticleToObjectNode.hx +++ b/leenkx/Sources/leenkx/logicnode/AddParticleToObjectNode.hx @@ -13,7 +13,7 @@ class AddParticleToObjectNode extends LogicNode { } override function run(from: Int) { - #if lnx_particles + #if lnx_gpu_particles if (property0 == 'Scene Active'){ var objFrom: Object = inputs[1].get(); @@ -47,7 +47,7 @@ class AddParticleToObjectNode extends LogicNode { var oslot: Int = mobjTo.particleSystems.length-1; var opsys = mobjTo.particleSystems[oslot]; - @:privateAccess opsys.setupGeomGpu(mobjTo.particleChildren[oslot], mobjTo); + @:privateAccess opsys.setupGeomGpu(mobjTo.particleChildren[oslot]); } else { var sceneName: String = inputs[1].get(); @@ -82,7 +82,7 @@ class AddParticleToObjectNode extends LogicNode { var oslot: Int = mobjTo.particleSystems.length-1; var opsys = mobjTo.particleSystems[oslot]; - @:privateAccess opsys.setupGeomGpu(mobjTo.particleChildren[oslot], mobjTo); + @:privateAccess opsys.setupGeomGpu(mobjTo.particleChildren[oslot]); break; } diff --git a/leenkx/Sources/leenkx/logicnode/AddPhysicsConstraintNode.hx b/leenkx/Sources/leenkx/logicnode/AddPhysicsConstraintNode.hx index 89b5f800..f1628147 100644 --- a/leenkx/Sources/leenkx/logicnode/AddPhysicsConstraintNode.hx +++ b/leenkx/Sources/leenkx/logicnode/AddPhysicsConstraintNode.hx @@ -4,6 +4,7 @@ import iron.object.Object; #if lnx_physics import leenkx.trait.physics.PhysicsConstraint; + import leenkx.trait.physics.PhysicsConstraint.ConstraintAxis; #if lnx_bullet import leenkx.trait.physics.bullet.PhysicsConstraint.ConstraintType; #elseif lnx_jolt @@ -31,7 +32,6 @@ class AddPhysicsConstraintNode extends LogicNode { if (pivotObject == null || rb1 == null || rb2 == null) return; #if lnx_physics - var disableCollisions: Bool = inputs[4].get(); var breakable: Bool = inputs[5].get(); var breakingThreshold: Float = inputs[6].get(); diff --git a/leenkx/Sources/leenkx/logicnode/DrawImageNode.hx b/leenkx/Sources/leenkx/logicnode/DrawImageNode.hx index 20d5f10c..faa37d9d 100644 --- a/leenkx/Sources/leenkx/logicnode/DrawImageNode.hx +++ b/leenkx/Sources/leenkx/logicnode/DrawImageNode.hx @@ -31,7 +31,7 @@ class DrawImageNode extends LogicNode { RenderToTexture.g.rotate(angle, x, y); - if (imgName != lastImgName) { + if (imgName != lastImgName || img == null) { // Load new image lastImgName = imgName; iron.data.Data.getImage(imgName, (image: Image) -> { diff --git a/leenkx/Sources/leenkx/logicnode/DrawImageRenderNode.hx b/leenkx/Sources/leenkx/logicnode/DrawImageRenderNode.hx new file mode 100644 index 00000000..892b2728 --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/DrawImageRenderNode.hx @@ -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); + + } + +} diff --git a/leenkx/Sources/leenkx/logicnode/DrawSubImageNode.hx b/leenkx/Sources/leenkx/logicnode/DrawSubImageNode.hx index 4318c1c7..45ac74d3 100644 --- a/leenkx/Sources/leenkx/logicnode/DrawSubImageNode.hx +++ b/leenkx/Sources/leenkx/logicnode/DrawSubImageNode.hx @@ -14,7 +14,7 @@ class DrawSubImageNode extends LogicNode { } override function run(from: Int) { - RenderToTexture.ensure2DContext("DrawImageNode"); + RenderToTexture.ensure2DContext("DrawSubImageNode"); final imgName: String = inputs[1].get(); final colorVec: Vec4 = inputs[2].get(); @@ -32,12 +32,10 @@ class DrawSubImageNode extends LogicNode { final drawx = x - 0.5 * width * anchorH; 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); - if (imgName != lastImgName) { + if (imgName != lastImgName || img == null) { // Load new image lastImgName = imgName; 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.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); runOutput(0); diff --git a/leenkx/Sources/leenkx/logicnode/DrawToImageNode.hx b/leenkx/Sources/leenkx/logicnode/DrawToImageNode.hx new file mode 100644 index 00000000..032dbe50 --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/DrawToImageNode.hx @@ -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 + } + +} diff --git a/leenkx/Sources/leenkx/logicnode/DrawToScreenNode.hx b/leenkx/Sources/leenkx/logicnode/DrawToScreenNode.hx new file mode 100644 index 00000000..875c3003 --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/DrawToScreenNode.hx @@ -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(); + + } + + } + +} diff --git a/leenkx/Sources/leenkx/logicnode/EmitSignalNode.hx b/leenkx/Sources/leenkx/logicnode/EmitSignalNode.hx new file mode 100644 index 00000000..d16440d5 --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/EmitSignalNode.hx @@ -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 = []; + for (i in 2...inputs.length) { + args.push(inputs[i].get()); + } + + Reflect.callMethod(signal, Reflect.field(signal, "emit"), args); + runOutput(0); + } +} diff --git a/leenkx/Sources/leenkx/logicnode/GetImageColorNode.hx b/leenkx/Sources/leenkx/logicnode/GetImageColorNode.hx new file mode 100644 index 00000000..6d8064b1 --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/GetImageColorNode.hx @@ -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; + + } + +} diff --git a/leenkx/Sources/leenkx/logicnode/GetParticleDataNode.hx b/leenkx/Sources/leenkx/logicnode/GetParticleDataNode.hx index 5fdd8b36..50698252 100644 --- a/leenkx/Sources/leenkx/logicnode/GetParticleDataNode.hx +++ b/leenkx/Sources/leenkx/logicnode/GetParticleDataNode.hx @@ -14,7 +14,7 @@ class GetParticleDataNode extends LogicNode { if (object == null) return null; - #if lnx_particles + #if lnx_gpu_particles var mo = cast(object, iron.object.MeshObject); diff --git a/leenkx/Sources/leenkx/logicnode/GetParticleNode.hx b/leenkx/Sources/leenkx/logicnode/GetParticleNode.hx index 7b9b9c15..3c117c32 100644 --- a/leenkx/Sources/leenkx/logicnode/GetParticleNode.hx +++ b/leenkx/Sources/leenkx/logicnode/GetParticleNode.hx @@ -13,7 +13,7 @@ class GetParticleNode extends LogicNode { if (object == null) return null; - #if lnx_particles + #if lnx_gpu_particles var mo = cast(object, iron.object.MeshObject); diff --git a/leenkx/Sources/leenkx/logicnode/GetTilesheetFlipNode.hx b/leenkx/Sources/leenkx/logicnode/GetTilesheetFlipNode.hx new file mode 100644 index 00000000..cd9ddb88 --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/GetTilesheetFlipNode.hx @@ -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; + } +} diff --git a/leenkx/Sources/leenkx/logicnode/GetTilesheetStateNode.hx b/leenkx/Sources/leenkx/logicnode/GetTilesheetStateNode.hx index bc15d951..55c0aab7 100644 --- a/leenkx/Sources/leenkx/logicnode/GetTilesheetStateNode.hx +++ b/leenkx/Sources/leenkx/logicnode/GetTilesheetStateNode.hx @@ -11,13 +11,13 @@ class GetTilesheetStateNode extends LogicNode { override function get(from: Int): Dynamic { 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) { - case 0: tilesheet.raw.name; - case 1: tilesheet.action.name; + case 0: object.name; // Return object name since tilesheet is embedded + case 1: tilesheet.action != null ? tilesheet.action.name : null; case 2: tilesheet.getFrameOffset(); case 3: tilesheet.frame; case 4: tilesheet.paused; diff --git a/leenkx/Sources/leenkx/logicnode/GlobalSignalNode.hx b/leenkx/Sources/leenkx/logicnode/GlobalSignalNode.hx new file mode 100644 index 00000000..c08cff08 --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/GlobalSignalNode.hx @@ -0,0 +1,24 @@ +package leenkx.logicnode; + +import leenkx.system.Signal; + +class GlobalSignalNode extends LogicNode { + public static var signals: Map = new Map(); + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Null { + 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; + } +} diff --git a/leenkx/Sources/leenkx/logicnode/LogicTree.hx b/leenkx/Sources/leenkx/logicnode/LogicTree.hx index fa10b291..94fddab0 100644 --- a/leenkx/Sources/leenkx/logicnode/LogicTree.hx +++ b/leenkx/Sources/leenkx/logicnode/LogicTree.hx @@ -33,6 +33,7 @@ class LogicTree extends iron.Trait { if (paused) return; 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 (_lateUpdate != null) for (f in _lateUpdate) iron.App.removeLateUpdate(f); } @@ -41,6 +42,7 @@ class LogicTree extends iron.Trait { if (!paused) return; 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 (_lateUpdate != null) for (f in _lateUpdate) iron.App.notifyOnLateUpdate(f); } diff --git a/leenkx/Sources/leenkx/logicnode/OnContactArrayNode.hx b/leenkx/Sources/leenkx/logicnode/OnContactArrayNode.hx index 58372f23..05713146 100644 --- a/leenkx/Sources/leenkx/logicnode/OnContactArrayNode.hx +++ b/leenkx/Sources/leenkx/logicnode/OnContactArrayNode.hx @@ -11,10 +11,10 @@ class OnContactArrayNode extends LogicNode { public function new(tree: LogicTree) { super(tree); - tree.notifyOnUpdate(update); + tree.notifyOnFixedUpdate(fixedUpdate); } - function update() { + function fixedUpdate() { var object1: Object = inputs[0].get(); var objects: Array = inputs[1].get(); diff --git a/leenkx/Sources/leenkx/logicnode/OnContactNode.hx b/leenkx/Sources/leenkx/logicnode/OnContactNode.hx index 263f9cf7..4070c7ed 100644 --- a/leenkx/Sources/leenkx/logicnode/OnContactNode.hx +++ b/leenkx/Sources/leenkx/logicnode/OnContactNode.hx @@ -15,10 +15,10 @@ class OnContactNode extends LogicNode { public function new(tree: LogicTree) { super(tree); - tree.notifyOnUpdate(update); + tree.notifyOnFixedUpdate(fixedUpdate); } - function update() { + function fixedUpdate() { var object1: Object = inputs[0].get(); var object2: Object = inputs[1].get(); diff --git a/leenkx/Sources/leenkx/logicnode/OnSignalNode.hx b/leenkx/Sources/leenkx/logicnode/OnSignalNode.hx new file mode 100644 index 00000000..1dfca905 --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/OnSignalNode.hx @@ -0,0 +1,49 @@ +package leenkx.logicnode; + +import leenkx.system.Signal; + +class OnSignalNode extends LogicNode { + var emittedArgs: Array = []; + 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; + } + } +} diff --git a/leenkx/Sources/leenkx/logicnode/OnUpdateNode.hx b/leenkx/Sources/leenkx/logicnode/OnUpdateNode.hx index 1b220c91..13f7f413 100644 --- a/leenkx/Sources/leenkx/logicnode/OnUpdateNode.hx +++ b/leenkx/Sources/leenkx/logicnode/OnUpdateNode.hx @@ -4,7 +4,7 @@ import leenkx.trait.physics.PhysicsWorld; 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) { super(tree); @@ -13,6 +13,7 @@ class OnUpdateNode extends LogicNode { function init() { switch (property0) { + case "Fixed Update": tree.notifyOnFixedUpdate(update); case "Late Update": tree.notifyOnLateUpdate(update); #if lnx_physics case "Physics Pre-Update": PhysicsWorld.active.notifyOnPreUpdate(update); diff --git a/leenkx/Sources/leenkx/logicnode/PauseTilesheetNode.hx b/leenkx/Sources/leenkx/logicnode/PauseTilesheetNode.hx index 82920883..ce1716b6 100644 --- a/leenkx/Sources/leenkx/logicnode/PauseTilesheetNode.hx +++ b/leenkx/Sources/leenkx/logicnode/PauseTilesheetNode.hx @@ -11,9 +11,9 @@ class PauseTilesheetNode extends LogicNode { override function run(from: Int) { 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); } diff --git a/leenkx/Sources/leenkx/logicnode/PlayActionFromNode.hx b/leenkx/Sources/leenkx/logicnode/PlayActionFromNode.hx new file mode 100644 index 00000000..9fae6be7 --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/PlayActionFromNode.hx @@ -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 = []; + 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 = []; + + 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 = []; + 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); + + } +} diff --git a/leenkx/Sources/leenkx/logicnode/PlayTilesheetNode.hx b/leenkx/Sources/leenkx/logicnode/PlayTilesheetActionNode.hx similarity index 65% rename from leenkx/Sources/leenkx/logicnode/PlayTilesheetNode.hx rename to leenkx/Sources/leenkx/logicnode/PlayTilesheetActionNode.hx index af6eca86..57cf39db 100644 --- a/leenkx/Sources/leenkx/logicnode/PlayTilesheetNode.hx +++ b/leenkx/Sources/leenkx/logicnode/PlayTilesheetActionNode.hx @@ -2,7 +2,7 @@ package leenkx.logicnode; import iron.object.MeshObject; -class PlayTilesheetNode extends LogicNode { +class PlayTilesheetActionNode extends LogicNode { public function new(tree: LogicTree) { super(tree); @@ -12,9 +12,9 @@ class PlayTilesheetNode extends LogicNode { var object: MeshObject = inputs[1].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); }); diff --git a/leenkx/Sources/leenkx/logicnode/RemoveParticleFromObjectNode.hx b/leenkx/Sources/leenkx/logicnode/RemoveParticleFromObjectNode.hx index 573444ff..2bcb69d8 100644 --- a/leenkx/Sources/leenkx/logicnode/RemoveParticleFromObjectNode.hx +++ b/leenkx/Sources/leenkx/logicnode/RemoveParticleFromObjectNode.hx @@ -11,7 +11,7 @@ class RemoveParticleFromObjectNode extends LogicNode { } override function run(from: Int) { - #if lnx_particles + #if lnx_gpu_particles var object: Object = inputs[1].get(); if (object == null) return; diff --git a/leenkx/Sources/leenkx/logicnode/ReplaceObjectNode.hx b/leenkx/Sources/leenkx/logicnode/ReplaceObjectNode.hx new file mode 100644 index 00000000..71641db6 --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/ReplaceObjectNode.hx @@ -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); + } +} diff --git a/leenkx/Sources/leenkx/logicnode/ResumeTilesheetNode.hx b/leenkx/Sources/leenkx/logicnode/ResumeTilesheetNode.hx index a56fd26f..c1839527 100644 --- a/leenkx/Sources/leenkx/logicnode/ResumeTilesheetNode.hx +++ b/leenkx/Sources/leenkx/logicnode/ResumeTilesheetNode.hx @@ -11,9 +11,9 @@ class ResumeTilesheetNode extends LogicNode { override function run(from: Int) { 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); } diff --git a/leenkx/Sources/leenkx/logicnode/RetainValueNode.hx b/leenkx/Sources/leenkx/logicnode/RetainValueNode.hx index cf80285f..ede18e48 100644 --- a/leenkx/Sources/leenkx/logicnode/RetainValueNode.hx +++ b/leenkx/Sources/leenkx/logicnode/RetainValueNode.hx @@ -1,5 +1,9 @@ package leenkx.logicnode; +import iron.math.Quat; +import iron.math.Vec4; +import iron.math.Mat4; + class RetainValueNode extends LogicNode { var value: Dynamic = null; @@ -9,7 +13,19 @@ class RetainValueNode extends LogicNode { } 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); } diff --git a/leenkx/Sources/leenkx/logicnode/SetFirstPersonControllerNode.hx b/leenkx/Sources/leenkx/logicnode/SetFirstPersonControllerNode.hx new file mode 100644 index 00000000..236621a2 --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/SetFirstPersonControllerNode.hx @@ -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); + } + +} diff --git a/leenkx/Sources/leenkx/logicnode/SetOverheadPersonControllerNode.hx b/leenkx/Sources/leenkx/logicnode/SetOverheadPersonControllerNode.hx new file mode 100644 index 00000000..539cff2d --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/SetOverheadPersonControllerNode.hx @@ -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); + } + +} diff --git a/leenkx/Sources/leenkx/logicnode/SetParticleDataNode.hx b/leenkx/Sources/leenkx/logicnode/SetParticleDataNode.hx index d9cb727d..cbcfe6e5 100644 --- a/leenkx/Sources/leenkx/logicnode/SetParticleDataNode.hx +++ b/leenkx/Sources/leenkx/logicnode/SetParticleDataNode.hx @@ -11,7 +11,7 @@ class SetParticleDataNode extends LogicNode { } override function run(from: Int) { - #if lnx_particles + #if lnx_gpu_particles var object: Object = inputs[1].get(); var slot: Int = inputs[2].get(); @@ -41,7 +41,7 @@ class SetParticleDataNode extends LogicNode { var emit_from: Int = inputs[3].get(); if (emit_from == 0 || emit_from == 1 || emit_from == 2) { @: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': @:privateAccess psys.r.auto_start = inputs[3].get(); diff --git a/leenkx/Sources/leenkx/logicnode/SetParticleRenderEmitterNode.hx b/leenkx/Sources/leenkx/logicnode/SetParticleRenderEmitterNode.hx index 11259628..c90729b5 100644 --- a/leenkx/Sources/leenkx/logicnode/SetParticleRenderEmitterNode.hx +++ b/leenkx/Sources/leenkx/logicnode/SetParticleRenderEmitterNode.hx @@ -9,7 +9,7 @@ class SetParticleRenderEmitterNode extends LogicNode { } override function run(from: Int) { - #if lnx_particles + #if lnx_gpu_particles var object: Object = inputs[1].get(); if (object == null) return; diff --git a/leenkx/Sources/leenkx/logicnode/SetParticleSpeedNode.hx b/leenkx/Sources/leenkx/logicnode/SetParticleSpeedNode.hx index 5d36d617..39fc64a1 100644 --- a/leenkx/Sources/leenkx/logicnode/SetParticleSpeedNode.hx +++ b/leenkx/Sources/leenkx/logicnode/SetParticleSpeedNode.hx @@ -9,7 +9,7 @@ class SetParticleSpeedNode extends LogicNode { } override function run(from: Int) { - #if lnx_particles + #if lnx_gpu_particles var object: Object = inputs[1].get(); var slot: Int = inputs[2].get(); var speed: Float = inputs[3].get(); diff --git a/leenkx/Sources/leenkx/logicnode/SetTilesheetActionNode.hx b/leenkx/Sources/leenkx/logicnode/SetTilesheetActionNode.hx new file mode 100644 index 00000000..5f242405 --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/SetTilesheetActionNode.hx @@ -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); + } +} diff --git a/leenkx/Sources/leenkx/logicnode/SetActiveTilesheetNode.hx b/leenkx/Sources/leenkx/logicnode/SetTilesheetFlipNode.hx similarity index 52% rename from leenkx/Sources/leenkx/logicnode/SetActiveTilesheetNode.hx rename to leenkx/Sources/leenkx/logicnode/SetTilesheetFlipNode.hx index 5427606e..2cff1408 100644 --- a/leenkx/Sources/leenkx/logicnode/SetActiveTilesheetNode.hx +++ b/leenkx/Sources/leenkx/logicnode/SetTilesheetFlipNode.hx @@ -1,9 +1,8 @@ package leenkx.logicnode; -import iron.Scene; import iron.object.MeshObject; -class SetActiveTilesheetNode extends LogicNode { +class SetTilesheetFlipNode extends LogicNode { public function new(tree: LogicTree) { super(tree); @@ -11,12 +10,13 @@ class SetActiveTilesheetNode extends LogicNode { override function run(from: Int) { var object: MeshObject = inputs[1].get(); - var tilesheet: String = inputs[2].get(); - var action: String = inputs[3].get(); + var flipX: Bool = inputs[2].get(); + var flipY: Bool = inputs[3].get(); if (object == null) return; - - object.setActiveTilesheet(Scene.active.raw.name, tilesheet, action); + if (object.tilesheet == null) return; + object.tilesheet.flipX = flipX; + object.tilesheet.flipY = flipY; runOutput(0); } diff --git a/leenkx/Sources/leenkx/logicnode/SetTilesheetFrameNode.hx b/leenkx/Sources/leenkx/logicnode/SetTilesheetFrameNode.hx index c17810a2..421c840a 100644 --- a/leenkx/Sources/leenkx/logicnode/SetTilesheetFrameNode.hx +++ b/leenkx/Sources/leenkx/logicnode/SetTilesheetFrameNode.hx @@ -12,9 +12,9 @@ class SetTilesheetFrameNode extends LogicNode { var object: MeshObject = inputs[1].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); } diff --git a/leenkx/Sources/leenkx/logicnode/SetTilesheetPausedNode.hx b/leenkx/Sources/leenkx/logicnode/SetTilesheetPausedNode.hx index 059867da..5c29d435 100644 --- a/leenkx/Sources/leenkx/logicnode/SetTilesheetPausedNode.hx +++ b/leenkx/Sources/leenkx/logicnode/SetTilesheetPausedNode.hx @@ -12,9 +12,9 @@ class SetTilesheetPausedNode extends LogicNode { var object: MeshObject = inputs[1].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); } diff --git a/leenkx/Sources/leenkx/logicnode/ShutdownNode.hx b/leenkx/Sources/leenkx/logicnode/ShutdownNode.hx index 5230770e..9f0b39f4 100644 --- a/leenkx/Sources/leenkx/logicnode/ShutdownNode.hx +++ b/leenkx/Sources/leenkx/logicnode/ShutdownNode.hx @@ -8,5 +8,6 @@ class ShutdownNode extends LogicNode { override function run(from: Int) { kha.System.stop(); + runOutput(0); } } diff --git a/leenkx/Sources/leenkx/logicnode/SignalNode.hx b/leenkx/Sources/leenkx/logicnode/SignalNode.hx new file mode 100644 index 00000000..0547dd11 --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/SignalNode.hx @@ -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 { + 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; + } +} diff --git a/leenkx/Sources/leenkx/logicnode/WriteImageNode.hx b/leenkx/Sources/leenkx/logicnode/WriteImageNode.hx index 8fc0a8e7..22c1a9ef 100644 --- a/leenkx/Sources/leenkx/logicnode/WriteImageNode.hx +++ b/leenkx/Sources/leenkx/logicnode/WriteImageNode.hx @@ -1,6 +1,7 @@ package leenkx.logicnode; import iron.object.CameraObject; +import kha.Color; class WriteImageNode extends LogicNode { @@ -43,6 +44,34 @@ class WriteImageNode extends LogicNode { camera.renderTarget = oldRT; 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(); for (i in 0...pixels.length){ diff --git a/leenkx/Sources/leenkx/network/OpCode.hx b/leenkx/Sources/leenkx/network/OpCode.hx index 5996bdd3..cf1e9b91 100644 --- a/leenkx/Sources/leenkx/network/OpCode.hx +++ b/leenkx/Sources/leenkx/network/OpCode.hx @@ -1,6 +1,6 @@ package leenkx.network; -@:enum abstract OpCode(Int) { +enum abstract OpCode(Int) { var Continuation = 0x00; var Text = 0x01; var Binary = 0x02; diff --git a/leenkx/Sources/leenkx/network/Types.hx b/leenkx/Sources/leenkx/network/Types.hx index c7b2f9bd..180259d5 100644 --- a/leenkx/Sources/leenkx/network/Types.hx +++ b/leenkx/Sources/leenkx/network/Types.hx @@ -6,7 +6,7 @@ import haxe.io.Bytes; typedef BinaryType = js.html.BinaryType; #else -@:enum abstract BinaryType(String) { +enum abstract BinaryType(String) { var ARRAYBUFFER = "arraybuffer"; @:to public function toString() { diff --git a/leenkx/Sources/leenkx/renderpath/Inc.hx b/leenkx/Sources/leenkx/renderpath/Inc.hx index e79fe5fe..7198b988 100644 --- a/leenkx/Sources/leenkx/renderpath/Inc.hx +++ b/leenkx/Sources/leenkx/renderpath/Inc.hx @@ -10,8 +10,6 @@ class Inc { static var path: RenderPath; public static var superSample = 1.0; - static var pointIndex = 0; - static var spotIndex = 0; static var lastFrame = -1; #if lnx_shadowmap_atlas @@ -41,6 +39,7 @@ class Inc { #if (rp_voxels == "Voxel GI") static var voxel_td1:kha.compute.TextureUnit; static var voxel_te1:kha.compute.TextureUnit; + static var voxel_tf1:kha.compute.TextureUnit; static var voxel_cc1:kha.compute.ConstantLocation; #else #if lnx_voxelgi_shadows @@ -94,8 +93,34 @@ class Inc { static var voxel_cb4:kha.compute.ConstantLocation; static var voxel_cc4: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 //rp_voxels + #end + #end //rp_voxels == "Voxel GI" + #end //rp_voxels != "Off" public static function init(_path: RenderPath) { path = _path; @@ -440,33 +465,39 @@ class Inc { path.bindTarget(n, n); break; } - for (i in 0...pointIndex) { - var n = "shadowMapPoint[" + i + "]"; - path.bindTarget(n, n); - var n = "shadowMapPointTransparent[" + i + "]"; - path.bindTarget(n, n); - } - for (i in 0...spotIndex) { - var n = "shadowMapSpot[" + i + "]"; - path.bindTarget(n, n); - var n = "shadowMapSpotTransparent[" + i + "]"; - path.bindTarget(n, n); + var lightIndex = 0; + 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); + var n = "shadowMapPointTransparent[" + lightIndex + "]"; + path.bindTarget(n, n); + } + else if (l.data.raw.type == "spot" || l.data.raw.type == "area") { + var n = "shadowMapSpot[" + lightIndex + "]"; + path.bindTarget(n, n); + var n = "shadowMapSpotTransparent[" + lightIndex + "]"; + 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) { case "sun": return transparent ? "shadowMapTransparent" : "shadowMap"; case "point": - return transparent ? "shadowMapPointTransparent[" + pointIndex + "]" : "shadowMapPoint[" + pointIndex + "]"; + return transparent ? "shadowMapPointTransparent[" + index + "]" : "shadowMapPoint[" + index + "]"; default: - return transparent ? "shadowMapSpotTransparent[" + spotIndex + "]" : "shadowMapSpot[" + spotIndex + "]"; + return transparent ? "shadowMapSpotTransparent[" + index + "]" : "shadowMapSpot[" + index + "]"; } } - static function getShadowMap(l: iron.object.LightObject, transparent: Bool): String { - var target = shadowMapName(l, transparent); + static function getShadowMap(l: iron.object.LightObject, index: Int, transparent: Bool): String { + var target = shadowMapName(l, index, transparent); var rt = path.renderTargets.get(target); // Create shadowmap on the fly if (rt == null) { @@ -509,13 +540,12 @@ class Inc { lastFrame = RenderPath.active.frame; #end - pointIndex = 0; - spotIndex = 0; + var lightIndex = 0; for (l in iron.Scene.active.lights) { if (!l.visible) continue; 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; for (i in 0...faces) { if (faces > 1) path.currentFace = i; @@ -527,18 +557,18 @@ class Inc { } path.currentFace = -1; - if (l.data.raw.type == "point") pointIndex++; - else if (l.data.raw.type == "spot" || l.data.raw.type == "area") spotIndex++; + if (!iron.object.LightObject.discardLightCulled(l)) { + lightIndex++; + } } #if rp_shadowmap_transparent - pointIndex = 0; - spotIndex = 0; + lightIndex = 0; for (l in iron.Scene.active.lights) { if (!l.visible) continue; 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; for (i in 0...faces) { if (faces > 1) path.currentFace = i; @@ -550,8 +580,9 @@ class Inc { } path.currentFace = -1; - if (l.data.raw.type == "point") pointIndex++; - else if (l.data.raw.type == "spot" || l.data.raw.type == "area") spotIndex++; + if (!iron.object.LightObject.discardLightCulled(l)) { + lightIndex++; + } } #end #end // rp_shadowmap @@ -606,6 +637,9 @@ class Inc { // Init voxels #if (rp_voxels != 'Off') if (!voxelsCreated) initGI(); + #if (rp_voxels == "Voxel GI") + initGI("voxelsLight"); + #end #end #end // lnx_config } @@ -732,6 +766,12 @@ class Inc { t.height = res * Main.voxelgiClipmapCount; t.depth = res; } + else if (t.name == "voxelsLight") { + t.format = "RGBA32"; + t.width = res; + t.height = res * Main.voxelgiClipmapCount; + t.depth = res; + } else { #if (rp_voxels == "Voxel AO") { @@ -876,7 +916,8 @@ class Inc { #if (rp_voxels == "Voxel GI") 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"); #else #if lnx_voxelgi_shadows @@ -949,6 +990,37 @@ class Inc { voxel_cc4 = voxel_sh4.getConstantLocation("eye"); 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 } @@ -998,7 +1070,8 @@ class Inc { kha.compute.Compute.setTexture(voxel_tc1, rts.get("voxelsOut").image, kha.compute.Access.Write); #if (rp_voxels == "Voxel GI") 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); #else #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); } + #end public static function resolveSpecular() { 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); } + + #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 // Voxels } diff --git a/leenkx/Sources/leenkx/renderpath/Postprocess.hx b/leenkx/Sources/leenkx/renderpath/Postprocess.hx index f7cf7c00..cfa1e7f6 100644 --- a/leenkx/Sources/leenkx/renderpath/Postprocess.hx +++ b/leenkx/Sources/leenkx/renderpath/Postprocess.hx @@ -342,12 +342,6 @@ class Postprocess { v.x = ssao_uniforms[0]; //SSAO Strength v.y = ssao_uniforms[1]; //SSAO Radius 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": v = iron.object.Uniforms.helpVec; v.x = camera_uniforms[10]; //Distort @@ -377,6 +371,12 @@ class Postprocess { v.y = letterbox_uniforms[0][1]; v.z = letterbox_uniforms[0][2]; 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": v = iron.object.Uniforms.helpVec; v.x = sharpen_uniforms[0][0]; //Color diff --git a/leenkx/Sources/leenkx/renderpath/RenderPathDeferred.hx b/leenkx/Sources/leenkx/renderpath/RenderPathDeferred.hx index 258cce2a..9f562771 100644 --- a/leenkx/Sources/leenkx/renderpath/RenderPathDeferred.hx +++ b/leenkx/Sources/leenkx/renderpath/RenderPathDeferred.hx @@ -712,6 +712,7 @@ class RenderPathDeferred { Inc.computeVoxelsTemporal(); #if (rp_voxels == "Voxel GI") + Inc.computeVoxelsLight(); Inc.computeVoxelsSDF(); #end diff --git a/leenkx/Sources/leenkx/renderpath/RenderPathForward.hx b/leenkx/Sources/leenkx/renderpath/RenderPathForward.hx index 77ac39a4..17a34734 100644 --- a/leenkx/Sources/leenkx/renderpath/RenderPathForward.hx +++ b/leenkx/Sources/leenkx/renderpath/RenderPathForward.hx @@ -448,6 +448,10 @@ class RenderPathForward { Inc.computeVoxelsTemporal(); + #if (rp_voxels == "Voxel GI") + Inc.computeVoxelsLight(); + #end + #if (lnx_voxelgi_shadows || (rp_voxels == "Voxel GI")) Inc.computeVoxelsSDF(); #end diff --git a/leenkx/Sources/leenkx/system/Signal.hx b/leenkx/Sources/leenkx/system/Signal.hx index 0abbe95e..f297adeb 100644 --- a/leenkx/Sources/leenkx/system/Signal.hx +++ b/leenkx/Sources/leenkx/system/Signal.hx @@ -27,7 +27,7 @@ class Signal { return callbacks; } - public function isConnected(callBack: Function):Bool { + public function isConnected(callBack: Function): Bool { return callbacks.contains(callBack); } diff --git a/leenkx/Sources/leenkx/system/Starter.hx b/leenkx/Sources/leenkx/system/Starter.hx index c6d94a5d..427c65c4 100644 --- a/leenkx/Sources/leenkx/system/Starter.hx +++ b/leenkx/Sources/leenkx/system/Starter.hx @@ -60,7 +60,7 @@ class Starter { #if lnx_patch iron.Scene.getRenderPath = getRenderPath; #end - #if lnx_draworder_shader + #if lnx_draworder_index iron.RenderPath.active.drawOrder = iron.RenderPath.DrawOrder.Index; #end // else Distance }); diff --git a/leenkx/Sources/leenkx/system/Timer.hx b/leenkx/Sources/leenkx/system/Timer.hx new file mode 100644 index 00000000..c3dcf479 --- /dev/null +++ b/leenkx/Sources/leenkx/system/Timer.hx @@ -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; + } + } + } +} diff --git a/leenkx/Sources/leenkx/trait/FirstPersonController.hx b/leenkx/Sources/leenkx/trait/FirstPersonController.hx index 8f65e13a..51f5186c 100644 --- a/leenkx/Sources/leenkx/trait/FirstPersonController.hx +++ b/leenkx/Sources/leenkx/trait/FirstPersonController.hx @@ -86,11 +86,14 @@ class FirstPersonController extends Trait { 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() { if (Input.occupied || body == null) return; var mouse = Input.getMouse(); var kb = Input.getKeyboard(); + // Blooquear/Desbloquear cursor // lock/unlock cursor if (mouse.started() && !mouse.locked) mouse.lock(); else if (kb.started("escape") && mouse.locked) @@ -113,6 +116,7 @@ class FirstPersonController extends Trait { 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() { if (body == null) return; var deltaTime:Float = iron.system.Time.delta; @@ -132,14 +136,15 @@ class FirstPersonController extends Trait { } #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()) { canJump = true; } - // Saltar con estamina + // 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%. + // 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; @@ -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 (stamina) { if (staminaValue > 0.0) { @@ -175,7 +180,7 @@ class FirstPersonController extends Trait { isRunning = false; } - // (temporizadores aparte) + // (temporizadores aparte) (fatigue system timers) if (isRunning) { timeSinceStop = 0.0; fatigueTimer += deltaTime; @@ -185,24 +190,25 @@ class FirstPersonController extends Trait { fatigueCooldown += deltaTime; } - // Evitar correr y saltar al estar fatigado... + // Evitar correr y saltar al estar fatigado // Avoid running and jumping when fatigued if (isFatigued()) { - isRunning = false; - canJump = false; + 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 + // 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 + // 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; @@ -210,7 +216,7 @@ class FirstPersonController extends Trait { } } - // Movimiento ejes (local) + // Movimiento en ejes locales // Movement on local axies dir.set(0, 0, 0); if (moveForward) dir.add(object.transform.look()); if (moveBackward) dir.add(object.transform.look().mult(-1)); @@ -220,6 +226,7 @@ class FirstPersonController extends Trait { 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 (isMoving) { var dirN = dir.normalize(); var baseSpeed = moveSpeed; diff --git a/leenkx/Sources/leenkx/trait/OverheadPersonController.hx b/leenkx/Sources/leenkx/trait/OverheadPersonController.hx new file mode 100644 index 00000000..53014bd8 --- /dev/null +++ b/leenkx/Sources/leenkx/trait/OverheadPersonController.hx @@ -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..... diff --git a/leenkx/Sources/leenkx/trait/PhysicsBreak.hx b/leenkx/Sources/leenkx/trait/PhysicsBreak.hx index cf5ad571..d63f162b 100644 --- a/leenkx/Sources/leenkx/trait/PhysicsBreak.hx +++ b/leenkx/Sources/leenkx/trait/PhysicsBreak.hx @@ -6,60 +6,91 @@ import iron.Trait; import iron.object.MeshObject; import iron.data.MeshData; import iron.data.SceneFormat; -#if lnx_bullet -import leenkx.trait.physics.bullet.RigidBody; +#if (lnx_bullet || lnx_oimo) +import leenkx.trait.physics.RigidBody; import leenkx.trait.physics.PhysicsWorld; #end class PhysicsBreak extends Trait { -#if (!lnx_bullet) +#if (!lnx_bullet && !lnx_oimo) public function new() { super(); } #else - static var physics: PhysicsWorld = null; - static var breaker: ConvexBreaker = null; + // Track all debris for cleanup on scene change + static var allDebris: Array = []; + static var sceneCallbackRegistered = false; + var breaker: ConvexBreaker; + var physics: PhysicsWorld; var body: RigidBody; public function new() { super(); - if (breaker == null) breaker = new ConvexBreaker(); + breaker = new ConvexBreaker(); notifyOnInit(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); 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); } + 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() { + if (body == null || !body.ready || physics == null) return; + var ar = physics.getContactPairs(body); if (ar != null) { - var maxImpulse = 0.0; + var maxImpulse: Float = 0.0; var impactPoint: Vec4 = null; var impactNormal: Vec4 = null; for (p in ar) { if (maxImpulse < p.impulse) { maxImpulse = p.impulse; impactPoint = p.posB; + #if lnx_bullet impactNormal = p.normOnB; + #elseif lnx_oimo + impactNormal = p.nor; + #end } } + #if lnx_bullet 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 randIter = 1; var debris = breaker.subdivideByImpact(cast object, impactPoint, impactNormal, radialIter, randIter); // var numObjects = debris.length; for (o in debris) { var ud = breaker.userDataMap.get(cast o); + if (ud == null) continue; var params: RigidBodyParams = { linearDamping: 0.04, angularDamping: 0.1, @@ -72,7 +103,7 @@ class PhysicsBreak extends Trait { angularFactorsZ: 1.0, collisionMargin: 0.04, linearDeactivationThreshold: 0.0, - angularDeactivationThrshold: 0.0, + angularDeactivationThreshold: 0.0, deactivationTime: 0.0, linearVelocityMin: 0.0, linearVelocityMax: 0.0, @@ -89,7 +120,12 @@ class PhysicsBreak extends Trait { if (cast(o, MeshObject).data.geom.positions.values.length < 600) { 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(); } } @@ -485,22 +521,26 @@ class ConvexBreaker { var numObjects = 0; if (numPoints1 > 4) { var data1 = makeMeshData(points1); - object1 = new MeshObject(data1, object.materials); - object1.transform.loc.setFrom(tempCM1); - object1.transform.rot.setFrom(object.transform.rot); - object1.transform.buildMatrix(); - initBreakableObject(object1, newMass, userData.friction, userData.velocity, userData.angularVelocity, 2 * radius1 > minSizeForBreak); - numObjects++; + if (data1 != null) { + object1 = new MeshObject(data1, object.materials); + object1.transform.loc.setFrom(tempCM1); + object1.transform.rot.setFrom(object.transform.rot); + object1.transform.buildMatrix(); + initBreakableObject(object1, newMass, userData.friction, userData.velocity, userData.angularVelocity, 2 * radius1 > minSizeForBreak); + numObjects++; + } } if (numPoints2 > 4) { var data2 = makeMeshData(points2); - object2 = new MeshObject(data2, object.materials); - object2.transform.loc.setFrom(tempCM2); - object2.transform.rot.setFrom(object.transform.rot); - object2.transform.buildMatrix(); - initBreakableObject(object2, newMass, userData.friction, userData.velocity, userData.angularVelocity, 2 * radius2 > minSizeForBreak); - numObjects++; + if (data2 != null) { + object2 = new MeshObject(data2, object.materials); + object2.transform.loc.setFrom(tempCM2); + object2.transform.rot.setFrom(object.transform.rot); + object2.transform.buildMatrix(); + initBreakableObject(object2, newMass, userData.friction, userData.velocity, userData.angularVelocity, 2 * radius2 > minSizeForBreak); + numObjects++; + } } output.object1 = object1; @@ -510,9 +550,15 @@ class ConvexBreaker { static var meshIndex = 0; function makeMeshData(points: Array): MeshData { + // Need at least 4 points for a 3D hull + if (points.length < 4) return null; + while (points.length > 50) points.pop(); 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 pa = new Array(); var na = new Array(); diff --git a/leenkx/Sources/leenkx/trait/internal/DebugConsole.hx b/leenkx/Sources/leenkx/trait/internal/DebugConsole.hx index 1ab63cac..f9304e92 100644 --- a/leenkx/Sources/leenkx/trait/internal/DebugConsole.hx +++ b/leenkx/Sources/leenkx/trait/internal/DebugConsole.hx @@ -271,7 +271,7 @@ class DebugConsole extends Trait { 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._y -= ui.ELEMENT_OFFSET(); @@ -282,9 +282,9 @@ class DebugConsole extends Trait { var _y = ui._y; 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 - ui.text(object.uid+'_'+object.name); + ui.text('[ '+object.uid+' ] : '+object.name); if (object == iron.Scene.active.camera) { var tagWidth = 100; diff --git a/leenkx/Sources/leenkx/trait/physics/PhysicsConstraint.hx b/leenkx/Sources/leenkx/trait/physics/PhysicsConstraint.hx index eefdb485..1436b5e4 100644 --- a/leenkx/Sources/leenkx/trait/physics/PhysicsConstraint.hx +++ b/leenkx/Sources/leenkx/trait/physics/PhysicsConstraint.hx @@ -3,7 +3,7 @@ package leenkx.trait.physics; #if (!lnx_physics) 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 diff --git a/leenkx/Sources/leenkx/trait/physics/RigidBody.hx b/leenkx/Sources/leenkx/trait/physics/RigidBody.hx index fe0aceec..5894e1ee 100644 --- a/leenkx/Sources/leenkx/trait/physics/RigidBody.hx +++ b/leenkx/Sources/leenkx/trait/physics/RigidBody.hx @@ -3,19 +3,22 @@ package leenkx.trait.physics; #if (!lnx_physics) 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 #if lnx_bullet typedef RigidBody = leenkx.trait.physics.bullet.RigidBody; typedef Shape = leenkx.trait.physics.bullet.RigidBody.Shape; + typedef RigidBodyParams = leenkx.trait.physics.bullet.RigidBody.RigidBodyParams; #elseif lnx_jolt typedef RigidBody = leenkx.trait.physics.jolt.RigidBody; typedef Shape = leenkx.trait.physics.jolt.RigidBody.Shape; + typedef RigidBodyParams = leenkx.trait.physics.jolt.RigidBody.RigidBodyParams; #else typedef RigidBody = leenkx.trait.physics.oimo.RigidBody; typedef Shape = leenkx.trait.physics.oimo.RigidBody.Shape; + typedef RigidBodyParams = leenkx.trait.physics.oimo.RigidBody.RigidBodyParams; #end #end diff --git a/leenkx/Sources/leenkx/trait/physics/bullet/KinematicCharacterController.hx b/leenkx/Sources/leenkx/trait/physics/bullet/KinematicCharacterController.hx index 1c138c6f..cfc1049e 100644 --- a/leenkx/Sources/leenkx/trait/physics/bullet/KinematicCharacterController.hx +++ b/leenkx/Sources/leenkx/trait/physics/bullet/KinematicCharacterController.hx @@ -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 Sphere = 1; var ConvexHull = 2; @@ -355,7 +355,7 @@ class KinematicCharacterController extends Trait { var Capsule = 5; } -@:enum abstract ControllerActivationState(Int) from Int to Int { +enum abstract ControllerActivationState(Int) from Int to Int { var Active = 1; var NoDeactivation = 4; var NoSimulation = 5; diff --git a/leenkx/Sources/leenkx/trait/physics/bullet/PhysicsConstraint.hx b/leenkx/Sources/leenkx/trait/physics/bullet/PhysicsConstraint.hx index 435990df..391fd10e 100644 --- a/leenkx/Sources/leenkx/trait/physics/bullet/PhysicsConstraint.hx +++ b/leenkx/Sources/leenkx/trait/physics/bullet/PhysicsConstraint.hx @@ -36,24 +36,24 @@ class PhysicsConstraint extends iron.Trait { /** * Function to initialize physics constraint trait. - * - * @param object Pivot object to which this constraint trait will be added. The constraint limits are applied along the local axes of this object. This object need not - * be a Rigid Body. Typically an `Empty` object may be used. Moving/rotating/parenting this pivot object has no effect once the constraint trait is added. Removing - * the pivot object removes the constraint. - * - * @param body1 First rigid body to be constrained. This rigid body may be constrained by other constraints. - * - * @param body2 Second rigid body to be constrained. This rigid body may be constrained by other constraints. - * - * @param type Type of the constraint. - * - * @param disableCollisions Disable collisions between constrained objects. - * - * @param breakingThreshold Break the constraint if stress on this constraint exceeds this value. Set to 0 to make un-breakable. - * - * @param limits Constraint limits. This may be set before adding the trait to pivot object using the set limits functions. - * - **/ + * + * @param object Pivot object to which this constraint trait will be added. The constraint limits are applied along the local axes of this object. This object need not + * be a Rigid Body. Typically an `Empty` object may be used. Moving/rotating/parenting this pivot object has no effect once the constraint trait is added. Removing + * the pivot object removes the constraint. + * + * @param body1 First rigid body to be constrained. This rigid body may be constrained by other constraints. + * + * @param body2 Second rigid body to be constrained. This rigid body may be constrained by other constraints. + * + * @param type Type of the constraint. + * + * @param disableCollisions Disable collisions between constrained objects. + * + * @param breakingThreshold Break the constraint if stress on this constraint exceeds this value. Set to 0 to make un-breakable. + * + * @param limits Constraint limits. This may be set before adding the trait to pivot object using the set limits functions. + * + **/ public function new(body1: Object, body2: Object, type: ConstraintType, disableCollisions: Bool, breakingThreshold: Float, limits: Array = null) { super(); @@ -335,7 +335,7 @@ class PhysicsConstraint extends iron.Trait { if (breakingThreshold > 0) con.setBreakingImpulseThreshold(breakingThreshold); physics.addPhysicsConstraint(this); - + id = nextId; nextId++; @@ -350,11 +350,11 @@ class PhysicsConstraint extends iron.Trait { } /** - * Function to set constraint limits when using Hinge constraint. May be used after initalizing this trait but before adding it - * to the pivot object - **/ + * Function to set constraint limits when using Hinge constraint. May be used after initalizing this trait but before adding it + * to the pivot object + **/ public function setHingeConstraintLimits(angLimit: Bool, lowerAngLimit: Float, upperAngLimit: Float) { - + angLimit? limits[0] = 1 : limits[0] = 0; limits[1] = lowerAngLimit * (Math.PI/ 180); @@ -362,11 +362,11 @@ class PhysicsConstraint extends iron.Trait { } /** - * Function to set constraint limits when using Slider constraint. May be used after initalizing this trait but before adding it - * to the pivot object - **/ + * Function to set constraint limits when using Slider constraint. May be used after initalizing this trait but before adding it + * to the pivot object + **/ public function setSliderConstraintLimits(linLimit: Bool, lowerLinLimit: Float, upperLinLimit: Float) { - + linLimit? limits[0] = 1 : limits[0] = 0; limits[1] = lowerLinLimit; @@ -374,11 +374,11 @@ class PhysicsConstraint extends iron.Trait { } /** - * Function to set constraint limits when using Piston constraint. May be used after initalizing this trait but before adding it - * to the pivot object - **/ + * Function to set constraint limits when using Piston constraint. May be used after initalizing this trait but before adding it + * to the pivot object + **/ public function setPistonConstraintLimits(linLimit: Bool, lowerLinLimit: Float, upperLinLimit: Float, angLimit: Bool, lowerAngLimit: Float, upperAngLimit: Float) { - + linLimit? limits[0] = 1 : limits[0] = 0; limits[1] = lowerLinLimit; @@ -391,9 +391,9 @@ class PhysicsConstraint extends iron.Trait { } /** - * Function to set customized constraint limits when using Generic/ Generic Spring constraint. May be used after initalizing this trait but before adding it - * to the pivot object. Multiple constarints may be set by calling this function with different parameters. - **/ + * Function to set customized constraint limits when using Generic/ Generic Spring constraint. May be used after initalizing this trait but before adding it + * to the pivot object. Multiple constarints may be set by calling this function with different parameters. + **/ public function setGenericConstraintLimits(setLimit: Bool = false, lowerLimit: Float = 1.0, upperLimit: Float = -1.0, axis: ConstraintAxis = X, isAngular: Bool = false) { var i = 0; @@ -401,7 +401,7 @@ class PhysicsConstraint extends iron.Trait { var radian = (Math.PI/ 180); switch (axis){ - case X: + case X: i = 0; case Y: i = 3; @@ -420,16 +420,16 @@ class PhysicsConstraint extends iron.Trait { } /** - * Function to set customized spring parameters when using Generic/ Generic Spring constraint. May be used after initalizing this trait but before adding it - * to the pivot object. Multiple parameters to different axes may be set by calling this function with different parameters. - **/ + * Function to set customized spring parameters when using Generic/ Generic Spring constraint. May be used after initalizing this trait but before adding it + * to the pivot object. Multiple parameters to different axes may be set by calling this function with different parameters. + **/ public function setSpringParams(setSpring: Bool = false, stiffness: Float = 10.0, damping: Float = 0.5, axis: ConstraintAxis = X, isAngular: Bool = false) { var i = 0; var j = 0; switch (axis){ - case X: + case X: i = 18; case Y: i = 21; @@ -453,10 +453,9 @@ class PhysicsConstraint extends iron.Trait { #end } - } -@:enum abstract ConstraintType(Int) from Int to Int { +enum abstract ConstraintType(Int) from Int to Int { var Fixed = 0; var Point = 1; var Hinge = 2; @@ -467,7 +466,7 @@ class PhysicsConstraint extends iron.Trait { var Motor = 7; } -@:enum abstract ConstraintAxis(Int) from Int to Int { +enum abstract ConstraintAxis(Int) from Int to Int { var X = 0; var Y = 1; var Z = 2; diff --git a/leenkx/Sources/leenkx/trait/physics/bullet/PhysicsWorld.hx b/leenkx/Sources/leenkx/trait/physics/bullet/PhysicsWorld.hx index ba076d59..739d3bb1 100644 --- a/leenkx/Sources/leenkx/trait/physics/bullet/PhysicsWorld.hx +++ b/leenkx/Sources/leenkx/trait/physics/bullet/PhysicsWorld.hx @@ -1,7 +1,6 @@ package leenkx.trait.physics.bullet; #if lnx_bullet - import iron.Trait; import iron.system.Time; import iron.math.Vec4; @@ -10,7 +9,6 @@ import iron.math.RayCaster; import leenkx.trait.physics.PhysicsCache; class Hit { - public var rb: RigidBody; public var pos: Vec4; public var normal: Vec4; @@ -33,7 +31,6 @@ class ConvexHit { } class ContactPair { - public var a: Int; public var b: Int; public var posA: Vec4; @@ -48,7 +45,6 @@ class ContactPair { } class PhysicsWorld extends Trait { - public static var active: PhysicsWorld = null; static var sceneRemoved = false; @@ -207,6 +203,12 @@ class PhysicsWorld extends Trait { 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) { world.addConstraint(constraint.con, constraint.disableCollisions); conMap.set(constraint.id, constraint); @@ -319,7 +321,7 @@ class PhysicsWorld extends Trait { world.stepSimulation(t, currMaxSteps, Time.fixedStep); 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 physTime = kha.Scheduler.realTime() - startTime; diff --git a/leenkx/Sources/leenkx/trait/physics/bullet/RigidBody.hx b/leenkx/Sources/leenkx/trait/physics/bullet/RigidBody.hx index d4912bc5..4d19b556 100644 --- a/leenkx/Sources/leenkx/trait/physics/bullet/RigidBody.hx +++ b/leenkx/Sources/leenkx/trait/physics/bullet/RigidBody.hx @@ -68,6 +68,10 @@ class RigidBody extends iron.Trait { public var onReady: Void->Void = null; public var onContact: ArrayVoid> = null; public var heightData: haxe.io.Bytes = null; + + // Compound shape children (baked from exporter) + var compoundChildren: Array = null; + #if js static var ammoArray: Int = -1; #end @@ -92,7 +96,6 @@ class RigidBody extends iron.Trait { // Interpolation var interpolate: Bool = false; - var time: Float = 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 currentRot: bullet.Bt.Quaternion = new bullet.Bt.Quaternion(0, 0, 0, 1); @@ -131,7 +134,7 @@ class RigidBody extends iron.Trait { angularFactorsZ: 1.0, collisionMargin: 0.0, linearDeactivationThreshold: 0.0, - angularDeactivationThrshold: 0.0, + angularDeactivationThreshold: 0.0, deactivationTime: 0.0, linearVelocityMin: 0.0, linearVelocityMax: 0.0, @@ -160,7 +163,7 @@ class RigidBody extends iron.Trait { this.linearFactors = [params.linearFactorsX, params.linearFactorsY, params.linearFactorsZ]; this.angularFactors = [params.angularFactorsX, params.angularFactorsY, params.angularFactorsZ]; this.collisionMargin = params.collisionMargin; - this.deactivationParams = [params.linearDeactivationThreshold, params.angularDeactivationThrshold, params.deactivationTime]; + this.deactivationParams = [params.linearDeactivationThreshold, params.angularDeactivationThreshold, params.deactivationTime]; // New velocity limiting properties this.linearVelocityMin = params.linearVelocityMin; this.linearVelocityMax = params.linearVelocityMax; @@ -180,6 +183,9 @@ class RigidBody extends iron.Trait { this.staticObj = flags.staticObj; this.useDeactivation = flags.useDeactivation; + // Store compound children data if provided + this.compoundChildren = params.compoundChildren; + notifyOnAdd(init); } @@ -281,6 +287,25 @@ class RigidBody extends iron.Trait { btshape.setLocalScaling(vec1); #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(); vec1.setX(transform.worldx()); @@ -313,7 +338,7 @@ class RigidBody extends iron.Trait { bodyColl.setRollingFriction(angularFriction); bodyColl.setRestitution(restitution); - if ( useDeactivation) { + if (useDeactivation) { setDeactivationParams(deactivationParams[0], deactivationParams[1], deactivationParams[2]); } else { @@ -382,16 +407,49 @@ class RigidBody extends iron.Trait { #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() { if (interpolate) { - time += Time.delta; - - while (time >= Time.fixedStep) { - time -= Time.fixedStep; - } - - var t: Float = time / Time.fixedStep; + var t: Float = Time.fixedStepInterpolation; t = Helper.clamp(t, 0, 1); 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.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) { @@ -429,32 +484,30 @@ class RigidBody extends iron.Trait { var len = Math.sqrt(x * x + y * y + z * z + w * w); return new bullet.Bt.Quaternion(x / len, y / len, z / len, w / len); } - + function physicsUpdate() { if (!ready) return; + + prevPos.setValue(currentPos.x(), currentPos.y(), currentPos.z()); + prevRot.setValue(currentRot.x(), currentRot.y(), currentRot.z(), currentRot.w()); + if (animated) { syncTransform(); - } else { - if (interpolate) { - prevPos.setValue(currentPos.x(), currentPos.y(), currentPos.z()); - prevRot.setValue(currentRot.x(), currentRot.y(), currentRot.z(), currentRot.w()); - } - var trans = body.getWorldTransform(); - var p = trans.getOrigin(); - var q = trans.getRotation(); - transform.clearDelta(); - // transform.buildMatrix(); - currentPos.setValue(p.x(), p.y(), p.z()); - currentRot.setValue(q.x(), q.y(), q.z(), q.w()); - - - #if hl - p.delete(); - q.delete(); - trans.delete(); - #end } + var trans = body.getWorldTransform(); + var p = trans.getOrigin(); + var q = trans.getRotation(); + + currentPos.setValue(p.x(), p.y(), p.z()); + currentRot.setValue(q.x(), q.y(), q.z(), q.w()); + + #if hl + p.delete(); + q.delete(); + trans.delete(); + #end + if (onContact != null) { var rbs = physics.getContacts(this); if (rbs != null) for (rb in rbs) for (f in onContact) f(rb); @@ -533,6 +586,18 @@ class RigidBody extends iron.Trait { 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() { vec1.setValue(0, 0, 0); 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 Sphere = 1; var ConvexHull = 2; @@ -826,6 +891,7 @@ class RigidBody extends iron.Trait { var Cylinder = 5; var Capsule = 6; var Terrain = 7; + var Compound = 8; } typedef RigidBodyParams = { @@ -840,7 +906,7 @@ typedef RigidBodyParams = { var angularFactorsZ: Float; var collisionMargin: Float; var linearDeactivationThreshold: Float; - var angularDeactivationThrshold: Float; + var angularDeactivationThreshold: Float; var deactivationTime: Float; var linearVelocityMin: Float; var linearVelocityMax: Float; @@ -852,6 +918,21 @@ typedef RigidBodyParams = { var lockRotationX: Bool; var lockRotationY: Bool; var lockRotationZ: Bool; + @:optional var compoundChildren: Array; +} + +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 = { diff --git a/leenkx/Sources/leenkx/trait/physics/bullet/SoftBody.hx b/leenkx/Sources/leenkx/trait/physics/bullet/SoftBody.hx index 8cb944f8..f2359f2c 100644 --- a/leenkx/Sources/leenkx/trait/physics/bullet/SoftBody.hx +++ b/leenkx/Sources/leenkx/trait/physics/bullet/SoftBody.hx @@ -297,7 +297,7 @@ class SoftBody extends Trait { vertOffsetX /= numNodes; vertOffsetY /= numNodes; vertOffsetZ /= numNodes; - + //Setting the mean position as object local location mo.transform.scale.set(1, 1, 1); mo.transform.loc.set(vertOffsetX, vertOffsetY, vertOffsetZ); @@ -343,7 +343,7 @@ class SoftBody extends Trait { var mx = nodePos.x() - vertOffsetX; var my = nodePos.y() - vertOffsetY; var mz = nodePos.z() - vertOffsetZ; - + var nx = nodeNor.x(); var ny = nodeNor.y(); var nz = nodeNor.z(); @@ -423,7 +423,7 @@ class SoftBody extends Trait { #end } -@:enum abstract SoftShape(Int) from Int { +enum abstract SoftShape(Int) from Int { var Cloth = 0; var Volume = 1; } diff --git a/leenkx/Sources/leenkx/trait/physics/jolt/RigidBody.hx b/leenkx/Sources/leenkx/trait/physics/jolt/RigidBody.hx index 430fd358..909a2827 100644 --- a/leenkx/Sources/leenkx/trait/physics/jolt/RigidBody.hx +++ b/leenkx/Sources/leenkx/trait/physics/jolt/RigidBody.hx @@ -99,7 +99,7 @@ class RigidBody extends Trait { angularFactorsZ: 1.0, collisionMargin: 0.0, linearDeactivationThreshold: 0.0, - angularDeactivationThrshold: 0.0, + angularDeactivationThreshold: 0.0, deactivationTime: 0.0, linearVelocityMin: 0.0, linearVelocityMax: 0.0, @@ -152,6 +152,7 @@ class RigidBody extends Trait { return; transform = object.transform; + transform.buildMatrix(); physics = PhysicsWorld.active; if (physics == null) { @@ -720,7 +721,7 @@ typedef RigidBodyParams = { var angularFactorsZ:Float; var collisionMargin:Float; var linearDeactivationThreshold:Float; - var angularDeactivationThrshold:Float; + var angularDeactivationThreshold:Float; var deactivationTime:Float; var linearVelocityMin:Float; var linearVelocityMax:Float; diff --git a/leenkx/Sources/leenkx/ui/Canvas.hx b/leenkx/Sources/leenkx/ui/Canvas.hx index 44924e96..8059db81 100644 --- a/leenkx/Sources/leenkx/ui/Canvas.hx +++ b/leenkx/Sources/leenkx/ui/Canvas.hx @@ -254,7 +254,7 @@ class Canvas { if (element.editable == null) element.editable = true; zui.Ext.textArea(ui, h.nest(element.id), element.alignment, element.editable, getText(canvas, element), true); - + //handle does not change if (h.nest(element.id).changed) { var e = element.event; @@ -483,7 +483,7 @@ typedef TTranslatedText = { var text: String; } -@:enum abstract ElementType(Int) from Int to Int { +enum abstract ElementType(Int) from Int to Int { var Text = 0; var Image = 1; var Button = 2; @@ -507,7 +507,7 @@ typedef TTranslatedText = { var TextArea = 20; } -@:enum abstract Anchor(Int) from Int to Int { +enum abstract Anchor(Int) from Int to Int { var TopLeft = 0; var Top = 1; var TopRight = 2; diff --git a/leenkx/Sources/zui/Themes.hx b/leenkx/Sources/zui/Themes.hx index 84aaabaf..19e0e1e4 100644 --- a/leenkx/Sources/zui/Themes.hx +++ b/leenkx/Sources/zui/Themes.hx @@ -73,7 +73,7 @@ typedef TTheme = { 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 CubicBezier = 1; } diff --git a/leenkx/Sources/zui/Zui.hx b/leenkx/Sources/zui/Zui.hx index bb53d34f..76b4118a 100644 --- a/leenkx/Sources/zui/Zui.hx +++ b/leenkx/Sources/zui/Zui.hx @@ -1154,7 +1154,7 @@ class Zui { } } else handle.changed = false; - + #if (!kha_android && !kha_ios) if (handle == scrollHandle && inputDX != 0) { // Scroll #else @@ -1187,14 +1187,22 @@ class Zui { } if (submitTextHandle == handle) { submitTextEdit(); + var previousValue: Float = handle.value; #if js 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 - handle.value = Std.parseFloat(handle.text); + var parsedValue: Float = Std.parseFloat(handle.text); + handle.value = Math.isNaN(parsedValue) ? previousValue : parsedValue; #end + if (handle.value < from) handle.value = from; + else if (handle.value > to) handle.value = to; handle.changed = changed = true; } @@ -1335,7 +1343,7 @@ class Zui { var search = textSelected.toLowerCase(); while (comboSelectedTexts[comboToSubmit - step].toLowerCase().indexOf(search) < 0 && comboToSubmit - step > 0) ++step; - + // Corner case: Current position is the top one according to the search pattern. if (comboSelectedTexts[comboToSubmit - step].toLowerCase().indexOf(search) < 0) step = 0; } @@ -1352,7 +1360,7 @@ class Zui { // Corner case: Current position is the lowest one according to the search pattern. if (comboSelectedTexts[comboToSubmit + step].toLowerCase().indexOf(search) < 0) step = 0; } - + comboToSubmit += step; submitComboHandle = comboSelectedHandle; } @@ -2015,6 +2023,8 @@ typedef HandleOptions = { } class Handle { + static var ptrCounter: Int = 0; + public var ptr(default, null): Int; // Unique handle identifier public var selected = false; public var position = 0; public var color = kha.Color.White; @@ -2034,6 +2044,7 @@ class Handle { var children: Map; public function new(ops: HandleOptions = null) { + ptr = ptrCounter++; if (ops != null) { if (ops.selected != null) selected = ops.selected; if (ops.position != null) position = ops.position; @@ -2063,18 +2074,18 @@ class Handle { public static var global = new Handle(); } -@:enum abstract Layout(Int) from Int { +enum abstract Layout(Int) from Int { var Vertical = 0; var Horizontal = 1; } -@:enum abstract Align(Int) from Int { +enum abstract Align(Int) from Int { var Left = 0; var Center = 1; var Right = 2; } -@:enum abstract State(Int) from Int { +enum abstract State(Int) from Int { var Idle = 0; var Started = 1; var Down = 2; diff --git a/leenkx/blender/lnx/assets.py b/leenkx/blender/lnx/assets.py index e3380870..99864006 100644 --- a/leenkx/blender/lnx/assets.py +++ b/leenkx/blender/lnx/assets.py @@ -55,6 +55,16 @@ def reset(): shader_cons['voxel_frag'] = [] 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): global assets diff --git a/leenkx/blender/lnx/exporter.py b/leenkx/blender/lnx/exporter.py index e4077fe3..d497b68e 100644 --- a/leenkx/blender/lnx/exporter.py +++ b/leenkx/blender/lnx/exporter.py @@ -12,6 +12,7 @@ Attribution-ShareAlike 3.0 Unported License: https://creativecommons.org/licenses/by-sa/3.0/deed.en_US """ from enum import Enum, unique +import copy import math import os import time @@ -32,6 +33,7 @@ from mathutils import Matrix, Vector import bmesh import lnx.utils +import lnx.linked_utils as linked_utils import lnx.profiler from lnx import assets, exporter_opt, log, make_renderpath 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) mat_batch = lnx.reload_module(mat_batch) lnx.utils = lnx.reload_module(lnx.utils) + linked_utils = lnx.reload_module(linked_utils) lnx.profiler = lnx.reload_module(lnx.profiler) else: lnx.enable_reload(__name__) @@ -67,9 +70,16 @@ class NodeType(Enum): """Returns the NodeType enum member belonging to the type of the given blender object.""" if bobject.type == "MESH": - if bobject.data.polygons: + if bobject.data.polygons or bobject.data.edges or bobject.data.vertices: 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 elif bobject.type == "LIGHT": return cls.LIGHT @@ -148,6 +158,15 @@ class LeenkxExporter: self.referenced_collections: List[bpy.types.Collection] = [] """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 """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 calculation. """ - start = action.frame_range[0] - end = action.frame_range[1] + frame_range = action.frame_range + 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 # frame range @@ -331,22 +369,12 @@ class LeenkxExporter: def export_object_transform(self, bobject: bpy.types.Object, o): wrd = bpy.data.worlds['Lnx'] - # HACK: In Blender 4.2.x, each camera must be selected to ensure its matrix is correctly assigned - if bpy.app.version >= (4, 2, 0) and bobject.type == 'CAMERA' and bobject.users_scene: - current_scene = bpy.context.window.scene - - 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() + # Use TransformEvaluator to handle linked object transform evaluation + with linked_utils.TransformEvaluator(bobject, self.scene, self.depsgraph) as evaluator: + matrix_local = evaluator.matrix_local # Static transform - o['transform'] = {'values': LeenkxExporter.write_matrix(bobject.matrix_local)} + o['transform'] = {'values': LeenkxExporter.write_matrix(matrix_local)} # Animated transform 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: 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] = { "objectType": btype, - "structName": lnx.utils.asset_name(bobject) + "structName": struct_name } if bobject.type == "ARMATURE": @@ -491,7 +525,7 @@ class LeenkxExporter: fcurve_list = self.collect_bone_animation(armature, bone.name) 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': []} o['anim'] = {'tracks': [out_track]} @@ -549,7 +583,7 @@ class LeenkxExporter: def export_particle_system_ref(self, psys: bpy.types.ParticleSystem, out_object): if psys.settings.instance_object is None or psys.settings.render_type != 'OBJECT' or not psys.settings.instance_object.lnx_export: return - + for obj in bpy.data.objects: if obj.name == out_object['name']: for mod in obj.modifiers: @@ -610,7 +644,7 @@ class LeenkxExporter: return None 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: hidden_states = {} try: @@ -659,21 +693,31 @@ class LeenkxExporter: # Skinning if lnx.utils.export_bone_data(bobject): variant_suffix = '_lnxskin' - # Tilesheets - elif bobject.lnx_tilesheet != '': - if not bobject.lnx_use_custom_tilesheet_node: + # Tilesheets - check if object has tilesheet enabled + elif bobject.type == 'MESH' and len(bobject.material_slots) > 0: + if bobject.lnx_tilesheet_enabled: 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' if variant_suffix == '': continue + # For regular mesh objects, process their material slots for slot in bobject.material_slots: if slot.material is None: continue + # For linked materials, set the flag directly (can't create variants) 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 if slot.material.name.endswith(variant_suffix): continue @@ -690,6 +734,23 @@ class LeenkxExporter: matvars.append(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_sys: bpy.types.ParticleSettings for particle_sys in bpy.data.particles: @@ -698,7 +759,10 @@ class LeenkxExporter: continue 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 if slot.material.name.endswith('_lnxpart'): continue @@ -806,7 +870,7 @@ class LeenkxExporter: # self.indentLevel -= 1 # 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 includes its name, object reference, material references (for meshes), and transform. @@ -815,6 +879,70 @@ class LeenkxExporter: if not bobject.lnx_export: 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) if bobject_ref is not None: object_type = bobject_ref["objectType"] @@ -852,13 +980,53 @@ class LeenkxExporter: 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: out_object['group_ref'] = bobject.instance_collection.name self.referenced_collections.append(bobject.instance_collection) - if bobject.lnx_tilesheet != '': - out_object['tilesheet_ref'] = bobject.lnx_tilesheet - out_object['tilesheet_action_ref'] = bobject.lnx_tilesheet_action + # Export tilesheet data if enabled on this object + if bobject.lnx_tilesheet_enabled: + 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: out_object['vertex_groups'] = [] @@ -904,7 +1072,8 @@ class LeenkxExporter: for proplist_item in bobject.lnx_propertylist: # Check if the property is a collection (array type). 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 collection_value = getattr(proplist_item, 'array_prop') property_name = array_type + '_prop' @@ -923,7 +1092,7 @@ class LeenkxExporter: # Export the object reference and material references objref = bobject.data if objref is not None: - objname = lnx.utils.asset_name(objref) + objname = linked_utils.asset_name(objref) # LOD if bobject.type == 'MESH' and hasattr(objref, 'lnx_lodlist') and len(objref.lnx_lodlist) > 0: @@ -973,12 +1142,11 @@ class LeenkxExporter: out_object['particle_refs'] = [] out_object['render_emitter'] = bobject.show_instancer_for_render for i in range(num_psys): - for obj in bpy.data.objects: - for mod in obj.modifiers: - if mod.type == 'PARTICLE_SYSTEM': - if mod.particle_system.name == bobject.particle_systems[i].name: - if mod.show_render: - self.export_particle_system_ref(bobject.particle_systems[i], out_object) + for mod in bobject.modifiers: + if mod.type == 'PARTICLE_SYSTEM': + if mod.particle_system.name == bobject.particle_systems[i].name: + if mod.show_render: + self.export_particle_system_ref(bobject.particle_systems[i], out_object) aabb = bobject.data.lnx_aabb if aabb[0] == 0 and aabb[1] == 0 and aabb[2] == 0: @@ -1145,9 +1313,11 @@ class LeenkxExporter: _bake_hidden[_obj] = False _obj.hide_viewport = True + start, end = self.calculate_anim_frame_range(action) + bake_result = bpy.ops.nla.bake( - frame_start=int(action.frame_range[0]), - frame_end=int(action.frame_range[1]), + frame_start=start, + frame_end=end, step=1, only_selected=False, visual_keying=True @@ -1210,7 +1380,7 @@ class LeenkxExporter: 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'] = [] if bobject.lnx_instanced == 'Off': @@ -1650,6 +1820,7 @@ class LeenkxExporter: maxdim_uvlayer = lay0 _uv0_raw = np.empty(len(lay0.data) * 2, dtype=' 0 else 0.0 if _uv0_absmax > maxdim: maxdim = _uv0_absmax @@ -1657,6 +1828,7 @@ class LeenkxExporter: lay1 = uv_layers[t1map] _uv1_raw = np.empty(len(lay1.data) * 2, dtype=' 0 else 0.0 if _uv1_absmax > maxdim: maxdim = _uv1_absmax @@ -1666,6 +1838,7 @@ class LeenkxExporter: lay2 = uv_layers[morph_uv_index] _uv2_raw = np.empty(len(lay2.data) * 2, dtype=' 0 else 0.0 if _uv2_absmax > maxdim: maxdim = _uv2_absmax @@ -1919,16 +2092,27 @@ class LeenkxExporter: armature = bobject.find_armature() apply_modifiers = not armature - bobject_eval = bobject.evaluated_get(self.depsgraph) if apply_modifiers else bobject - export_mesh = bobject_eval.to_mesh() + if apply_modifiers: + # 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 if shape_keys: 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() - bobject_eval = bobject.evaluated_get(self.depsgraph) if apply_modifiers else bobject - export_mesh = bobject_eval.to_mesh() + if apply_modifiers: + # 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: log.warn(oid + ' was not exported') @@ -1980,7 +2164,30 @@ class LeenkxExporter: """Exports a single light object.""" rpdat = lnx.utils.get_rp() 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 + 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 = { 'name': object_ref[1]["structName"], 'type': objtype.lower(), @@ -1988,8 +2195,8 @@ class LeenkxExporter: 'near_plane': light_ref.lnx_clip_start, 'far_plane': light_ref.lnx_clip_end, 'fov': light_ref.lnx_fov, - 'color': [light_ref.color[0], light_ref.color[1], light_ref.color[2]], - 'strength': light_ref.energy, + 'color': color, + 'strength': strength, 'shadows_bias': light_ref.lnx_shadows_bias * 0.0001 } if rpdat.rp_shadows: @@ -2001,25 +2208,37 @@ class LeenkxExporter: out_light['shadowmap_size'] = 0 if objtype == 'SUN': - out_light['strength'] *= 0.325 # Scale bias for ortho light matrix out_light['shadows_bias'] *= 20.0 if out_light['shadowmap_size'] > 1024: # Less bias for bigger maps out_light['shadows_bias'] *= 1 / (out_light['shadowmap_size'] / 1024) 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['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 elif objtype == 'SPOT': - out_light['strength'] *= 0.01 - out_light['spot_size'] = math.cos(light_ref.spot_size / 2) - # Cycles defaults to 0.15 - out_light['spot_blend'] = light_ref.spot_blend / 10 + out_light['strength'] *= 1.0 / (4.0 * math.pi) + half_angle = light_ref.spot_size * 0.5 + outer_cos = math.cos(half_angle) + 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': - 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_y'] = light_ref.size_y @@ -2052,11 +2271,22 @@ class LeenkxExporter: if not bobject.lnx_export: 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 # outside the collection, then instantiate the full object # child tree if the collection gets spawned as a whole if bobject.parent is None or bobject.parent.name not in collection.objects: - asset_name = lnx.utils.asset_name(bobject) + 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) if collection.library and not collection.name in self.scene.collection.children: # Add external linked objects @@ -2067,14 +2297,18 @@ class LeenkxExporter: # otherwise Blender will not allow duplicate names if asset_name in scene_objects: log.warn("skipping export of the object" - f" {bobject.name} (collection" - f" {collection.name}) because it has the same" - " export name as another object in the scene:" - f" {asset_name}") + f" {bobject.name} (collection" + f" {collection.name}) because it has the same" + " export name as another object in the scene:" + f" {asset_name}") continue 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) @@ -2275,7 +2509,7 @@ class LeenkxExporter: material.signature = signature o = {} - o['name'] = lnx.utils.asset_name(material) + o['name'] = lnx.linked_utils.asset_name(material) if material.lnx_skip_context != '': o['skip_context'] = material.lnx_skip_context @@ -2385,15 +2619,18 @@ class LeenkxExporter: if len(self.particle_system_array) > 0: self.output['particle_datas'] = [] for particleRef in self.particle_system_array.items(): - padd = False; + + padd = False for obj in bpy.data.objects: for mod in obj.modifiers: if mod.type == 'PARTICLE_SYSTEM': if mod.particle_system.settings.name == particleRef[1]["structName"]: if mod.show_render: padd = True + if not padd: - continue; + continue + psettings = particleRef[0] if psettings is None: @@ -2408,12 +2645,41 @@ class LeenkxExporter: elif psettings.emit_from == 'VOLUME': 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 = { 'name': particleRef[1]["structName"], 'type': 0 if psettings.type == 'EMITTER' else 1, # HAIR 'auto_start': psettings.lnx_auto_start, 'dynamic_emitter': psettings.lnx_dynamic_emitter, 'is_unique': psettings.lnx_is_unique, + 'local_coords': psettings.lnx_local_coords, 'loop': psettings.lnx_loop, # Emission 'count': int(psettings.count * psettings.lnx_count_mult), @@ -2433,6 +2699,13 @@ class LeenkxExporter: ), # 'object_factor': psettings.object_factor, '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_type': 1 if psettings.physics_type == 'NEWTON' else 0, 'particle_size': psettings.particle_size, @@ -2441,7 +2714,10 @@ class LeenkxExporter: # Render 'instance_object': lnx.utils.asset_name(psettings.instance_object), # 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: @@ -2453,25 +2729,47 @@ class LeenkxExporter: self.output['particle_datas'].append(out_particlesys) - def export_tilesheets(self): - wrd = bpy.data.worlds['Lnx'] - if len(wrd.lnx_tilesheetlist) > 0: - self.output['tilesheet_datas'] = [] - for ts in wrd.lnx_tilesheetlist: - o = {} - o['name'] = ts.name - o['tilesx'] = ts.tilesx_prop - o['tilesy'] = ts.tilesy_prop - o['framerate'] = ts.framerate_prop - o['actions'] = [] - for tsa in ts.lnx_tilesheetactionlist: - ao = {} - ao['name'] = tsa.name - ao['start'] = tsa.start_prop - ao['end'] = tsa.end_prop - ao['loop'] = tsa.loop_prop - o['actions'].append(ao) - self.output['tilesheet_datas'].append(o) + # For CPU particles + def extract_props(self, bpy_struct, depth=0, max_depth=2): + result = {} + for prop in bpy_struct.bl_rna.properties: + name = prop.identifier + if name == "rna_type": + continue + try: + value = getattr(bpy_struct, name) + + if name == "color_ramp" and hasattr(value, "elements"): + result[name] = { + "elements": [ + { + "position": el.position, + "color": { + "r": el.color[0], + "g": el.color[1], + "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"" + + return result def export_world(self): """Exports the world of the current scene.""" @@ -2562,7 +2860,7 @@ class LeenkxExporter: 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'): self.output['name'] += '.lz4' elif not bpy.data.worlds['Lnx'].lnx_minimize: @@ -2627,12 +2925,13 @@ class LeenkxExporter: if collection.name.startswith(('RigidBodyWorld', 'Trait|')): continue - if self.scene.user_of_id(collection) or collection in self.referenced_collections: - self.export_collection(collection) + 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) if not LeenkxExporter.option_mesh_only: 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: if self.scene.name == lnx.utils.get_project_scene_name(): log.warn(f'Scene "{self.scene.name}" is missing a camera') @@ -2653,7 +2952,6 @@ class LeenkxExporter: self.export_particle_systems() self.output['world_datas'] = [] self.export_world() - self.export_tilesheets() 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) @@ -2852,101 +3150,148 @@ class LeenkxExporter: # Rigid body trait if bobject.rigid_body is not None and phys_enabled: - LeenkxExporter.export_physics = True - rb = bobject.rigid_body - shape = 0 # BOX - - if rb.collision_shape == 'SPHERE': - shape = 1 - elif rb.collision_shape == 'CONVEX_HULL': - shape = 2 - elif rb.collision_shape == 'MESH': - shape = 3 - elif rb.collision_shape == 'CONE': - shape = 4 - elif rb.collision_shape == 'CYLINDER': - shape = 5 - elif rb.collision_shape == 'CAPSULE': - shape = 6 - - body_mass = rb.mass - is_static = self.rigid_body_static(rb) - if is_static: - body_mass = 0 - x = {} - x['type'] = 'Script' - x['class_name'] = 'leenkx.trait.physics.' + phys_pkg + '.RigidBody' - col_group = '' - for b in rb.collision_collections: - col_group = ('1' if b else '0') + col_group - col_mask = '' - for b in bobject.lnx_rb_collision_filter_mask: - col_mask = ('1' if b else '0') + col_mask - - x['parameters'] = [str(shape), str(body_mass), str(rb.friction), str(rb.restitution), str(int(col_group, 2)), str(int(col_mask, 2)) ] - lx = bobject.lnx_rb_linear_factor[0] - ly = bobject.lnx_rb_linear_factor[1] - lz = bobject.lnx_rb_linear_factor[2] - ax = bobject.lnx_rb_angular_factor[0] - ay = bobject.lnx_rb_angular_factor[1] - az = bobject.lnx_rb_angular_factor[2] - if bobject.lock_location[0]: - lx = 0 - if bobject.lock_location[1]: - ly = 0 - if bobject.lock_location[2]: - lz = 0 - if bobject.lock_rotation[0]: - ax = 0 - if bobject.lock_rotation[1]: - ay = 0 - if bobject.lock_rotation[2]: - az = 0 - col_margin = rb.collision_margin if rb.use_margin else 0.0 - if rb.use_deactivation: - deact_lv = rb.deactivate_linear_velocity - deact_av = rb.deactivate_angular_velocity - deact_time = bobject.lnx_rb_deactivation_time + # 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: - deact_lv = 0.0 - deact_av = 0.0 - deact_time = 0.0 - body_params = {} - body_params['linearDamping'] = rb.linear_damping - body_params['angularDamping'] = rb.angular_damping - body_params['linearFactorsX'] = lx - body_params['linearFactorsY'] = ly - body_params['linearFactorsZ'] = lz - body_params['angularFactorsX'] = ax - body_params['angularFactorsY'] = ay - body_params['angularFactorsZ'] = az - body_params['angularFriction'] = bobject.lnx_rb_angular_friction - body_params['collisionMargin'] = col_margin - body_params['linearDeactivationThreshold'] = deact_lv - body_params['angularDeactivationThrshold'] = deact_av - body_params['deactivationTime'] = deact_time - # New velocity limit properties - body_params['linearVelocityMin'] = bobject.lnx_rb_linear_velocity_min - body_params['linearVelocityMax'] = bobject.lnx_rb_linear_velocity_max - body_params['angularVelocityMin'] = bobject.lnx_rb_angular_velocity_min - body_params['angularVelocityMax'] = bobject.lnx_rb_angular_velocity_max - # New lock properties - body_params['lockTranslationX'] = bobject.lnx_rb_lock_translation_x - body_params['lockTranslationY'] = bobject.lnx_rb_lock_translation_y - body_params['lockTranslationZ'] = bobject.lnx_rb_lock_translation_z - body_params['lockRotationX'] = bobject.lnx_rb_lock_rotation_x - body_params['lockRotationY'] = bobject.lnx_rb_lock_rotation_y - body_params['lockRotationZ'] = bobject.lnx_rb_lock_rotation_z - body_flags = {} - body_flags['animated'] = rb.kinematic - body_flags['trigger'] = bobject.lnx_rb_trigger - body_flags['ccd'] = bobject.lnx_rb_ccd - body_flags['interpolate'] = bobject.lnx_rb_interpolate - body_flags['staticObj'] = is_static - body_flags['useDeactivation'] = rb.use_deactivation - x['parameters'].append(lnx.utils.get_haxe_json_string(body_params)) - x['parameters'].append(lnx.utils.get_haxe_json_string(body_flags)) - o['traits'].append(x) + LeenkxExporter.export_physics = True + rb = bobject.rigid_body + shape = 0 # BOX + + if rb.collision_shape == 'SPHERE': + shape = 1 + elif rb.collision_shape == 'CONVEX_HULL': + shape = 2 + elif rb.collision_shape == 'MESH': + shape = 3 + elif rb.collision_shape == 'CONE': + shape = 4 + elif rb.collision_shape == 'CYLINDER': + shape = 5 + elif rb.collision_shape == 'CAPSULE': + shape = 6 + elif rb.collision_shape == 'COMPOUND': + shape = 8 + + body_mass = rb.mass + is_static = self.rigid_body_static(rb) + if is_static: + body_mass = 0 + x = {} + x['type'] = 'Script' + x['class_name'] = 'leenkx.trait.physics.' + phys_pkg + '.RigidBody' + col_group = '' + for b in rb.collision_collections: + col_group = ('1' if b else '0') + col_group + col_mask = '' + for b in bobject.lnx_rb_collision_filter_mask: + col_mask = ('1' if b else '0') + col_mask + + x['parameters'] = [str(shape), str(body_mass), str(rb.friction), str(rb.restitution), str(int(col_group, 2)), str(int(col_mask, 2)) ] + lx = bobject.lnx_rb_linear_factor[0] + ly = bobject.lnx_rb_linear_factor[1] + lz = bobject.lnx_rb_linear_factor[2] + ax = bobject.lnx_rb_angular_factor[0] + ay = bobject.lnx_rb_angular_factor[1] + az = bobject.lnx_rb_angular_factor[2] + if bobject.lock_location[0]: + lx = 0 + if bobject.lock_location[1]: + ly = 0 + if bobject.lock_location[2]: + lz = 0 + if bobject.lock_rotation[0]: + ax = 0 + if bobject.lock_rotation[1]: + ay = 0 + if bobject.lock_rotation[2]: + az = 0 + col_margin = rb.collision_margin if rb.use_margin else 0.0 + if rb.use_deactivation: + deact_lv = rb.deactivate_linear_velocity + deact_av = rb.deactivate_angular_velocity + deact_time = bobject.lnx_rb_deactivation_time + else: + deact_lv = 0.0 + deact_av = 0.0 + deact_time = 0.0 + body_params = {} + body_params['linearDamping'] = rb.linear_damping + body_params['angularDamping'] = rb.angular_damping + body_params['linearFactorsX'] = lx + body_params['linearFactorsY'] = ly + body_params['linearFactorsZ'] = lz + body_params['angularFactorsX'] = ax + body_params['angularFactorsY'] = ay + body_params['angularFactorsZ'] = az + body_params['angularFriction'] = bobject.lnx_rb_angular_friction + body_params['collisionMargin'] = col_margin + body_params['linearDeactivationThreshold'] = deact_lv + body_params['angularDeactivationThreshold'] = deact_av + body_params['deactivationTime'] = deact_time + # New velocity limit properties + body_params['linearVelocityMin'] = bobject.lnx_rb_linear_velocity_min + body_params['linearVelocityMax'] = bobject.lnx_rb_linear_velocity_max + body_params['angularVelocityMin'] = bobject.lnx_rb_angular_velocity_min + body_params['angularVelocityMax'] = bobject.lnx_rb_angular_velocity_max + # New lock properties + body_params['lockTranslationX'] = bobject.lnx_rb_lock_translation_x + body_params['lockTranslationY'] = bobject.lnx_rb_lock_translation_y + body_params['lockTranslationZ'] = bobject.lnx_rb_lock_translation_z + body_params['lockRotationX'] = bobject.lnx_rb_lock_rotation_x + body_params['lockRotationY'] = bobject.lnx_rb_lock_rotation_y + 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['animated'] = rb.kinematic + body_flags['trigger'] = bobject.lnx_rb_trigger + body_flags['ccd'] = bobject.lnx_rb_ccd + body_flags['interpolate'] = bobject.lnx_rb_interpolate + body_flags['staticObj'] = is_static + body_flags['useDeactivation'] = rb.use_deactivation + x['parameters'].append(lnx.utils.get_haxe_json_string(body_params)) + x['parameters'].append(lnx.utils.get_haxe_json_string(body_flags)) + o['traits'].append(x) # Phys traits if phys_enabled: @@ -3106,7 +3451,7 @@ class LeenkxExporter: apply_modifiers = not armature 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: for v in export_mesh.vertices: @@ -3139,8 +3484,6 @@ class LeenkxExporter: if trait_prop.type.endswith("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: value = trait_prop.get_value() @@ -3466,7 +3809,7 @@ class LeenkxExporter: strength = world.lnx_envtex_strength mobile_mat = rpdat.lnx_material_model in ('Mobile', 'Solid') - if mobile_mat: + if mobile_mat or '_EnvCol' in world.world_defs: lnx_radiance = False out_probe = {'name': lnx.utils.asset_name(world) if world.library else world.name} diff --git a/leenkx/blender/lnx/exporter_opt.py b/leenkx/blender/lnx/exporter_opt.py index 91386e3b..98276a42 100644 --- a/leenkx/blender/lnx/exporter_opt.py +++ b/leenkx/blender/lnx/exporter_opt.py @@ -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 num_uv_layers -= 1 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): has_tex = True 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: if abs(v.uv[0]) > maxdim: maxdim = abs(v.uv[0]) - if abs(v.uv[1]) > maxdim: - maxdim = abs(v.uv[1]) + if abs(1.0 - v.uv[1]) > maxdim: + maxdim = abs(1.0 - v.uv[1]) if has_tex1: lay1 = uv_layers[t1map] for v in lay1.data: if abs(v.uv[0]) > maxdim: maxdim = abs(v.uv[0]) maxdim_uvlayer = lay1 - if abs(v.uv[1]) > maxdim: - maxdim = abs(v.uv[1]) + if abs(1.0 - v.uv[1]) > maxdim: + maxdim = abs(1.0 - v.uv[1]) maxdim_uvlayer = lay1 if has_morph_target: morph_data = np.empty(num_verts * 2, dtype=' maxdim: maxdim = abs(v.uv[0]) maxdim_uvlayer = lay2 - if abs(v.uv[1]) > maxdim: - maxdim = abs(v.uv[1]) + if abs(1.0 - v.uv[1]) > maxdim: + maxdim = abs(1.0 - v.uv[1]) maxdim_uvlayer = lay2 if maxdim > 1: o['scale_tex'] = maxdim diff --git a/leenkx/blender/lnx/lightmapper/utility/cycles/nodes.py b/leenkx/blender/lnx/lightmapper/utility/cycles/nodes.py index fda6133c..55d77e33 100644 --- a/leenkx/blender/lnx/lightmapper/utility/cycles/nodes.py +++ b/leenkx/blender/lnx/lightmapper/utility/cycles/nodes.py @@ -344,7 +344,7 @@ def apply_materials(load_atlas=0): else: 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(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": mat.node_tree.links.new(UVLightmap.outputs[0], lightmapNode.inputs[0]) #Connect uvnode to lightmapnode diff --git a/leenkx/blender/lnx/lightmapper/utility/cycles/prepare.py b/leenkx/blender/lnx/lightmapper/utility/cycles/prepare.py index 1615d75d..302d85f0 100644 --- a/leenkx/blender/lnx/lightmapper/utility/cycles/prepare.py +++ b/leenkx/blender/lnx/lightmapper/utility/cycles/prepare.py @@ -811,7 +811,7 @@ def set_settings(): 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": scene.cycles.tile_size = 256 else: diff --git a/leenkx/blender/lnx/linked_utils.py b/leenkx/blender/lnx/linked_utils.py new file mode 100644 index 00000000..693c1fae --- /dev/null +++ b/leenkx/blender/lnx/linked_utils.py @@ -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 diff --git a/leenkx/blender/lnx/logicnode/__init__.py b/leenkx/blender/lnx/logicnode/__init__.py index ab82a336..2d938bdc 100644 --- a/leenkx/blender/lnx/logicnode/__init__.py +++ b/leenkx/blender/lnx/logicnode/__init__.py @@ -28,6 +28,8 @@ def init_categories(): lnx_nodes.add_category('Logic', icon='OUTLINER', section="basic", 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('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('Native', icon='MEMORY', section="basic", description="The Native category contains nodes which interact with the system (Input/Output functionality, etc.) or Haxe.") diff --git a/leenkx/blender/lnx/logicnode/animation/LN_get_tilesheet_flip.py b/leenkx/blender/lnx/logicnode/animation/LN_get_tilesheet_flip.py new file mode 100644 index 00000000..ef3f7a07 --- /dev/null +++ b/leenkx/blender/lnx/logicnode/animation/LN_get_tilesheet_flip.py @@ -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') diff --git a/leenkx/blender/lnx/logicnode/animation/LN_get_tilesheet_state.py b/leenkx/blender/lnx/logicnode/animation/LN_get_tilesheet_state.py index 7bc7fe99..e3d4aa77 100644 --- a/leenkx/blender/lnx/logicnode/animation/LN_get_tilesheet_state.py +++ b/leenkx/blender/lnx/logicnode/animation/LN_get_tilesheet_state.py @@ -2,8 +2,8 @@ from lnx.logicnode.lnx_nodes import * class GetTilesheetStateNode(LnxLogicTreeNode): """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. @@ -15,23 +15,28 @@ class GetTilesheetStateNode(LnxLogicTreeNode): """ bl_idname = 'LNGetTilesheetStateNode' bl_label = 'Get Tilesheet State' - lnx_version = 2 + lnx_version = 4 lnx_section = 'tilesheet' def lnx_init(self, context): 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('LnxIntSocket', 'Frame') self.add_output('LnxIntSocket', 'Absolute Frame') self.add_output('LnxBoolSocket', 'Is Paused') def get_replacement_node(self, node_tree: bpy.types.NodeTree): - if self.lnx_version not in (0, 1): - raise LookupError() - - return NodeReplacement( - 'LNGetTilesheetStateNode', self.lnx_version, 'LNGetTilesheetStateNode', 2, - in_socket_mapping={}, out_socket_mapping={0:1, 1:3, 2:4} - ) + if self.lnx_version in (0, 1): + return NodeReplacement( + 'LNGetTilesheetStateNode', self.lnx_version, 'LNGetTilesheetStateNode', 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() diff --git a/leenkx/blender/lnx/logicnode/animation/LN_play_tilesheet.py b/leenkx/blender/lnx/logicnode/animation/LN_play_tilesheet_action.py similarity index 67% rename from leenkx/blender/lnx/logicnode/animation/LN_play_tilesheet.py rename to leenkx/blender/lnx/logicnode/animation/LN_play_tilesheet_action.py index fc46045d..80f80f8e 100644 --- a/leenkx/blender/lnx/logicnode/animation/LN_play_tilesheet.py +++ b/leenkx/blender/lnx/logicnode/animation/LN_play_tilesheet_action.py @@ -1,16 +1,16 @@ from lnx.logicnode.lnx_nodes import * -class PlayTilesheetNode(LnxLogicTreeNode): +class PlayTilesheetActionNode(LnxLogicTreeNode): """Plays the given tilesheet action.""" - bl_idname = 'LNPlayTilesheetNode' - bl_label = 'Play Tilesheet' + bl_idname = 'LNPlayTilesheetActionNode' + bl_label = 'Play Tilesheet Action' lnx_version = 1 lnx_section = 'tilesheet' def lnx_init(self, context): self.add_input('LnxNodeSocketAction', 'In') self.add_input('LnxNodeSocketObject', 'Object') - self.add_input('LnxStringSocket', 'Name') + self.add_input('LnxStringSocket', 'Action') self.add_output('LnxNodeSocketAction', 'Out') self.add_output('LnxNodeSocketAction', 'Done') diff --git a/leenkx/blender/lnx/logicnode/animation/LN_set_active_tilesheet.py b/leenkx/blender/lnx/logicnode/animation/LN_set_tilesheet_action.py similarity index 50% rename from leenkx/blender/lnx/logicnode/animation/LN_set_active_tilesheet.py rename to leenkx/blender/lnx/logicnode/animation/LN_set_tilesheet_action.py index f6c53b72..cf471c6b 100644 --- a/leenkx/blender/lnx/logicnode/animation/LN_set_active_tilesheet.py +++ b/leenkx/blender/lnx/logicnode/animation/LN_set_tilesheet_action.py @@ -1,16 +1,15 @@ from lnx.logicnode.lnx_nodes import * -class SetActiveTilesheetNode(LnxLogicTreeNode): - """Set the active tilesheet.""" - bl_idname = 'LNSetActiveTilesheetNode' - bl_label = 'Set Active Tilesheet' +class SetTilesheetActionNode(LnxLogicTreeNode): + """Sets the tilesheet action for the given object.""" + bl_idname = 'LNSetTilesheetActionNode' + bl_label = 'Set Tilesheet Action' lnx_version = 1 lnx_section = 'tilesheet' def lnx_init(self, context): self.add_input('LnxNodeSocketAction', 'In') self.add_input('LnxNodeSocketObject', 'Object') - self.add_input('LnxStringSocket', 'Tilesheet') self.add_input('LnxStringSocket', 'Action') - self.add_output('LnxNodeSocketAction', 'Out') \ No newline at end of file + self.add_output('LnxNodeSocketAction', 'Out') diff --git a/leenkx/blender/lnx/logicnode/animation/LN_set_tilesheet_flip.py b/leenkx/blender/lnx/logicnode/animation/LN_set_tilesheet_flip.py new file mode 100644 index 00000000..82a3ce38 --- /dev/null +++ b/leenkx/blender/lnx/logicnode/animation/LN_set_tilesheet_flip.py @@ -0,0 +1,21 @@ +from lnx.logicnode.lnx_nodes import * + +class SetTilesheetFlipNode(LnxLogicTreeNode): + """Set the flip state of the tilesheet for UV-based sprite flipping. + This is useful for billboarded sprites where mesh scaling cannot be used. + + @input Flip X: Flip the sprite horizontally. + @input Flip Y: Flip the sprite vertically. + """ + bl_idname = 'LNSetTilesheetFlipNode' + bl_label = 'Set Tilesheet Flip' + lnx_version = 1 + lnx_section = 'tilesheet' + + def lnx_init(self, context): + self.add_input('LnxNodeSocketAction', 'In') + self.add_input('LnxNodeSocketObject', 'Object') + self.add_input('LnxBoolSocket', 'Flip X') + self.add_input('LnxBoolSocket', 'Flip Y') + + self.add_output('LnxNodeSocketAction', 'Out') diff --git a/leenkx/blender/lnx/logicnode/draw/LN_draw_image_render.py b/leenkx/blender/lnx/logicnode/draw/LN_draw_image_render.py new file mode 100644 index 00000000..643adac8 --- /dev/null +++ b/leenkx/blender/lnx/logicnode/draw/LN_draw_image_render.py @@ -0,0 +1,54 @@ +from lnx.logicnode.lnx_nodes import * + + +class DrawImageRenderNode(LnxLogicTreeNode): + """Draws an image render target. + + @input Draw: Activate to draw the image on this frame. The input must + be (indirectly) called from an `On Render2D` node. + + @input In: Activate to retrieve the imaga render target. + @input Camera: the render target image of the camera. + @input Color: The color that the image's pixels are multiplied with. + @input Left/Center/Right: Horizontal anchor point of the image. + + 0 = Left, 1 = Center, 2 = Right + @input Top/Middle/Bottom: Vertical anchor point of the image. + + 0 = Top, 1 = Middle, 2 = Bottom + @input X/Y: Position of the anchor point in pixels. + @input Width/Height: Size of the image in pixels. + @input sX/Y: Position of the sub anchor point in pixels. + @input sWidth/sHeight: Size of the sub image in pixels. + @input Angle: Rotation angle in radians. Image will be rotated cloclwiswe + at the anchor point. + @input Render2D: include Render 2D draws. + + @output Out: Activated after the image has been drawn. + + @see [`kha.graphics2.Graphics.drawImage()`](http://kha.tech/api/kha/graphics2/Graphics.html#drawImage). + """ + bl_idname = 'LNDrawImageRenderNode' + bl_label = 'Draw Image Render' + lnx_section = 'draw' + lnx_version = 1 + + def lnx_init(self, context): + self.add_input('LnxNodeSocketAction', 'Draw') + self.add_input('LnxNodeSocketAction', 'In') + self.add_input('LnxNodeSocketObject', 'Camera') + self.add_input('LnxColorSocket', 'Color', default_value=[1.0, 1.0, 1.0, 1.0]) + self.add_input('LnxIntSocket', '0/1/2 = Left/Center/Right', default_value=0) + self.add_input('LnxIntSocket', '0/1/2 = Top/Middle/Bottom', default_value=0) + self.add_input('LnxFloatSocket', 'X') + self.add_input('LnxFloatSocket', 'Y') + self.add_input('LnxFloatSocket', 'Width') + self.add_input('LnxFloatSocket', 'Height') + self.add_input('LnxFloatSocket', 'sX') + self.add_input('LnxFloatSocket', 'sY') + self.add_input('LnxFloatSocket', 'sWidth') + self.add_input('LnxFloatSocket', 'sHeight') + self.add_input('LnxFloatSocket', 'Angle') + self.add_input('LnxBoolSocket', 'Render2D') + + self.add_output('LnxNodeSocketAction', 'Out') diff --git a/leenkx/blender/lnx/logicnode/draw/LN_draw_sub_image.py b/leenkx/blender/lnx/logicnode/draw/LN_draw_sub_image.py index 7559e61f..a204842f 100644 --- a/leenkx/blender/lnx/logicnode/draw/LN_draw_sub_image.py +++ b/leenkx/blender/lnx/logicnode/draw/LN_draw_sub_image.py @@ -12,9 +12,9 @@ class DrawSubImageNode(LnxLogicTreeNode): @input Top/Middle/Bottom: Vertical anchor point of the image. 0 = Top, 1 = Middle, 2 = Bottom @input X/Y: Position of the anchor point in pixels. - @input Width/Height: Size of the sub image in pixels. - @input sX/Y: Position of the sub anchor point in pixels. - @input sWidth/Height: Size of the image in pixels. + @input Width/Height: Size of the image in pixels. + @input sX/sY: Position of the sub anchor point in pixels. + @input sWidth/sHeight: Size of the sub image in pixels. @input Angle: Rotation angle in radians. Image will be rotated cloclwiswe at the anchor point. @output Out: Activated after the image has been drawn. diff --git a/leenkx/blender/lnx/logicnode/draw/LN_draw_to_image.py b/leenkx/blender/lnx/logicnode/draw/LN_draw_to_image.py new file mode 100644 index 00000000..f3e7c688 --- /dev/null +++ b/leenkx/blender/lnx/logicnode/draw/LN_draw_to_image.py @@ -0,0 +1,37 @@ +from lnx.logicnode.lnx_nodes import * + + +class DrawToImageNode(LnxLogicTreeNode): + """Writes the given draw image to the given file. If the image + already exists, the existing content of the image is overwritten. + + @input Image File: the name of the image + @input Color: The color that the image's pixels are multiplied with. + @input Width: width of the image file. + @input Height: heigth of the image file. + @input sX: sub position of first x pixel of the sub image (0 for start). + @input sY: sub position of first y pixel of the sub image (0 for start). + @input sWidth: width of the sub image. + @input sHeight: height of the sub image. + + WARNING: Calling getPixels() on a renderTarget with non-standard non-POT dimensions + can cause a system crash. Ensure renderTarget resolution is a power of two + (e.g., 256x256) or a common standard resolution (e.g., 1920x1080). + """ + bl_idname = 'LNDrawToImageNode' + bl_label = 'Draw To Image' + lnx_section = 'draw' + lnx_version = 1 + + def lnx_init(self, context): + self.add_input('LnxNodeSocketAction', 'In') + self.add_input('LnxStringSocket', 'Image File') + self.add_input('LnxColorSocket', 'Color', default_value=[1.0, 1.0, 1.0, 1.0]) + self.add_input('LnxIntSocket', 'Width') + self.add_input('LnxIntSocket', 'Height') + self.add_input('LnxIntSocket', 'sX') + self.add_input('LnxIntSocket', 'sY') + self.add_input('LnxIntSocket', 'sWidth') + self.add_input('LnxIntSocket', 'sHeight') + + self.add_output('LnxNodeSocketAction', 'Out') diff --git a/leenkx/blender/lnx/logicnode/draw/LN_draw_to_screen.py b/leenkx/blender/lnx/logicnode/draw/LN_draw_to_screen.py new file mode 100644 index 00000000..831a678f --- /dev/null +++ b/leenkx/blender/lnx/logicnode/draw/LN_draw_to_screen.py @@ -0,0 +1,69 @@ +from lnx.logicnode.lnx_nodes import * +import bpy + + +class DrawToScreenNode(LnxLogicTreeNode): + """Draws a Render Target image to screen. + + @input Draw: Activate to draw the Render Target image to screen. The input must + be (indirectly) called from an `On Render2D` node. + @input In: Activate to get the Render Target image of the render 2d draws. + @input Draw Width/Height: Size of the Render Target image in pixels. + @input Image: The filename of the image. + @input Color: The color that the image's pixels are multiplied with. + @input Left/Center/Right: Horizontal anchor point of the image. + + 0 = Left, 1 = Center, 2 = Right + @input Top/Middle/Bottom: Vertical anchor point of the image. + + 0 = Top, 1 = Middle, 2 = Bottom + @input X/Y: Position of the anchor point in pixels. + @input Width/Height: Size of the sub image in pixels. + @input sX/Y: Position of the sub anchor point in pixels. + @input sWidth/Height: Size of the image in pixels. + @input Angle: Rotation angle in radians. Image will be rotated cloclwiswe + at the anchor point. + @input Clear Image: Clear the image before drawing to it + + @output Out: Activated after the image has been drawn. + @output Draw: Input for the render 2d draws. + + @see [`kha.graphics2.Graphics.drawImage()`](http://kha.tech/api/kha/graphics2/Graphics.html#drawImage). + """ + bl_idname = 'LNDrawToScreenNode' + bl_label = 'Draw To Screen' + lnx_section = 'draw' + lnx_version = 2 + + def lnx_init(self, context): + self.add_input('LnxNodeSocketAction', 'Draw') + self.add_input('LnxNodeSocketAction', 'In') + self.add_input('LnxIntSocket', 'Draw Width') + self.add_input('LnxIntSocket', 'Draw Height') + self.add_input('LnxColorSocket', 'Color', default_value=[1.0, 1.0, 1.0, 1.0]) + self.add_input('LnxIntSocket', '0/1/2 = Left/Center/Right', default_value=0) + self.add_input('LnxIntSocket', '0/1/2 = Top/Middle/Bottom', default_value=0) + self.add_input('LnxFloatSocket', 'X') + self.add_input('LnxFloatSocket', 'Y') + self.add_input('LnxFloatSocket', 'Width') + self.add_input('LnxFloatSocket', 'Height') + self.add_input('LnxFloatSocket', 'sX') + self.add_input('LnxFloatSocket', 'sY') + self.add_input('LnxFloatSocket', 'sWidth') + self.add_input('LnxFloatSocket', 'sHeight') + self.add_input('LnxFloatSocket', 'Angle') + self.add_input('LnxBoolSocket', 'Clear Image') + + self.add_output('LnxNodeSocketAction', 'Out') + self.add_output('LnxNodeSocketAction', 'Draw') + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.lnx_version not in (0, 1): + raise LookupError() + + return NodeReplacement( + "LNDrawToScreenNode", self.lnx_version, + "LNDrawToScreenNode", 2, + in_socket_mapping={0:1, 1:2, 2:3, 3:4, 4:5, 5:6, 6:7, 7:8, 8:9, 9:10, 10:11, 11:12, 12:13, 13:14, 14:15}, + out_socket_mapping={0:0}, + ) diff --git a/leenkx/blender/lnx/logicnode/event/LN_on_update.py b/leenkx/blender/lnx/logicnode/event/LN_on_update.py index 46d56bb9..b805e8a8 100644 --- a/leenkx/blender/lnx/logicnode/event/LN_on_update.py +++ b/leenkx/blender/lnx/logicnode/event/LN_on_update.py @@ -4,6 +4,7 @@ class OnUpdateNode(LnxLogicTreeNode): """Activates the output on every frame. @option Update: (default) activates the output every frame. + @option Fixed Update: activates the output at a fixed time step. @option Late Update: activates the output after all non-late updates are calculated. @option Physics Pre-Update: activates the output before calculating the physics. Only available when using a physics engine.""" @@ -13,6 +14,7 @@ class OnUpdateNode(LnxLogicTreeNode): property0: HaxeEnumProperty( 'property0', items = [('Update', 'Update', 'Update'), + ('Fixed Update', 'Fixed Update', 'Fixed Update'), ('Late Update', 'Late Update', 'Late Update'), ('Physics Pre-Update', 'Physics Pre-Update', 'Physics Pre-Update')], name='On', default='Update') diff --git a/leenkx/blender/lnx/logicnode/lnx_sockets.py b/leenkx/blender/lnx/logicnode/lnx_sockets.py index 637266dd..839fe4b5 100644 --- a/leenkx/blender/lnx/logicnode/lnx_sockets.py +++ b/leenkx/blender/lnx/logicnode/lnx_sockets.py @@ -88,7 +88,7 @@ class LnxAnimActionSocket(LnxCustomSocket): default_value_raw: PointerProperty(name='Action', type=bpy.types.Action, update=_on_update_socket) def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + super(LnxAnimActionSocket, self).__init__(*args, **kwargs) if self.default_value_get is not None: self.default_value_raw = self.default_value_get self.default_value_get = None @@ -510,7 +510,7 @@ class LnxObjectSocket(LnxCustomSocket): default_value_raw: PointerProperty(name='Object', type=bpy.types.Object, update=_on_update_socket) def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + super(LnxObjectSocket, self).__init__(*args, **kwargs) if self.default_value_get is not None: self.default_value_raw = self.default_value_get self.default_value_get = None diff --git a/leenkx/blender/lnx/logicnode/logic/LN_select_output.py b/leenkx/blender/lnx/logicnode/logic/LN_select_output.py index 730348ac..6dbac9df 100644 --- a/leenkx/blender/lnx/logicnode/logic/LN_select_output.py +++ b/leenkx/blender/lnx/logicnode/logic/LN_select_output.py @@ -15,8 +15,8 @@ class SelectOutputNode(LnxLogicTreeNode): lnx_version = 1 min_outputs = 2 - def __init__(self): - super(SelectOutputNode, self).__init__() + def __init__(self, *args, **kwargs): + super(SelectOutputNode, self).__init__(*args, **kwargs) array_nodes[self.get_id_str()] = self def lnx_init(self, context): diff --git a/leenkx/blender/lnx/logicnode/postprocess/LN_set_camera_post_process.py b/leenkx/blender/lnx/logicnode/postprocess/LN_set_camera_post_process.py index 41155b2a..b1711a41 100644 --- a/leenkx/blender/lnx/logicnode/postprocess/LN_set_camera_post_process.py +++ b/leenkx/blender/lnx/logicnode/postprocess/LN_set_camera_post_process.py @@ -29,7 +29,7 @@ class CameraSetNode(LnxLogicTreeNode): if self.property0 == 'DoF F-Stop': self.add_input('LnxFloatSocket', 'DoF F-Stop', default_value=128.0)#8 if self.property0 == 'Tonemapping': - self.add_input('LnxIntSocket', 'Tonemapping', default_value=5)#9 + self.add_input('LnxIntSocket', 'Tonemapping', default_value=0)#9 if self.property0 == 'Distort': self.add_input('LnxFloatSocket', 'Distort', default_value=2.0)#10 if self.property0 == 'Film Grain': @@ -75,12 +75,12 @@ class CameraSetNode(LnxLogicTreeNode): layout.label(text="1: Filmic2") layout.label(text="2: Reinhard") layout.label(text="3: Uncharted2") - layout.label(text="5: Agx") - layout.label(text="6: None") + layout.label(text="5: None") + layout.prop(self, 'property0') def get_replacement_node(self, node_tree: bpy.types.NodeTree): - if self.lnx_version not in range(0, 4): + if self.lnx_version not in range(0, 6): raise LookupError() elif self.lnx_version == 1: newnode = node_tree.nodes.new('LNCameraSetNode') diff --git a/leenkx/blender/lnx/logicnode/random/LN_probabilistic_index.py b/leenkx/blender/lnx/logicnode/random/LN_probabilistic_index.py index 6c68a234..bf653912 100644 --- a/leenkx/blender/lnx/logicnode/random/LN_probabilistic_index.py +++ b/leenkx/blender/lnx/logicnode/random/LN_probabilistic_index.py @@ -17,7 +17,8 @@ class ProbabilisticIndexNode(LnxLogicTreeNode): num_choices: IntProperty(default=0, min=0) - def __init__(self): + def __init__(self, *args, **kwargs): + super(ProbabilisticIndexNode, self).__init__(*args, **kwargs) array_nodes[str(id(self))] = self def lnx_init(self, context): diff --git a/leenkx/blender/lnx/logicnode/signal/LN_emit_signal.py b/leenkx/blender/lnx/logicnode/signal/LN_emit_signal.py new file mode 100644 index 00000000..19ad7714 --- /dev/null +++ b/leenkx/blender/lnx/logicnode/signal/LN_emit_signal.py @@ -0,0 +1,45 @@ +from lnx.logicnode.lnx_nodes import * + + +class EmitSignalNode(LnxLogicTreeNode): + """Emits a Signal with optional arguments. + + Connect a Signal instance to the Signal input. When this node is activated, + it calls emit() on the Signal, passing any connected arguments to all + connected OnSignal nodes. + + Use 'Add Arg' to add input sockets for passing data to listeners. + + @seeNode Signal + @seeNode On Signal""" + + bl_idname = 'LNEmitSignalNode' + bl_label = 'Emit Signal' + lnx_version = 1 + lnx_section = 'signal' + min_inputs = 2 + + + def __init__(self, *args, **kwargs): + super(EmitSignalNode, self).__init__(*args, **kwargs) + array_nodes[str(id(self))] = self + + + def lnx_init(self, context): + self.add_input('LnxNodeSocketAction', 'In') + self.add_input('LnxDynamicSocket', 'Signal') + self.add_output('LnxNodeSocketAction', 'Out') + + + def draw_buttons(self, context, layout): + row = layout.row(align=True) + op = row.operator('lnx.node_add_input', text='Add Arg', icon='PLUS', emboss=True) + op.node_index = str(id(self)) + op.socket_type = 'LnxDynamicSocket' + op.name_format = "Arg {0}" + op.index_name_offset = -1 + column = row.column(align=True) + op = column.operator('lnx.node_remove_input', text='', icon='X', emboss=True) + op.node_index = str(id(self)) + if len(self.inputs) == self.min_inputs: + column.enabled = False diff --git a/leenkx/blender/lnx/logicnode/signal/LN_global_signal.py b/leenkx/blender/lnx/logicnode/signal/LN_global_signal.py new file mode 100644 index 00000000..d883fc2d --- /dev/null +++ b/leenkx/blender/lnx/logicnode/signal/LN_global_signal.py @@ -0,0 +1,25 @@ +from lnx.logicnode.lnx_nodes import * + + +class GlobalSignalNode(LnxLogicTreeNode): + """Gets or creates a global Signal by name. + + Global Signals are stored in a static registry and can be accessed from + any logic tree in the scene. Provide a unique name to identify the signal. + + Use this for communication between different objects or logic trees without + needing to pass Signal references directly. + + @seeNode Signal + @seeNode On Signal + @seeNode Emit Signal""" + + bl_idname = 'LNGlobalSignalNode' + bl_label = 'Global Signal' + lnx_version = 1 + lnx_section = 'signal' + + + def lnx_init(self, context): + self.add_input('LnxStringSocket', 'Property') + self.add_output('LnxDynamicSocket', 'Signal') diff --git a/leenkx/blender/lnx/logicnode/signal/LN_on_signal.py b/leenkx/blender/lnx/logicnode/signal/LN_on_signal.py new file mode 100644 index 00000000..08c73570 --- /dev/null +++ b/leenkx/blender/lnx/logicnode/signal/LN_on_signal.py @@ -0,0 +1,44 @@ +from lnx.logicnode.lnx_nodes import * + + +class OnSignalNode(LnxLogicTreeNode): + """Activates the output when the given Signal emits. + + Connect a Signal instance to the input. When that Signal emits, + the output is activated and emitted arguments are available on + the dynamic output sockets. + + Use 'Add Arg' to add output sockets for receiving emitted data. + + @seeNode Signal + @seeNode Emit Signal""" + + bl_idname = 'LNOnSignalNode' + bl_label = 'On Signal' + lnx_version = 1 + lnx_section = 'signal' + min_outputs = 1 + + + def __init__(self, *args, **kwargs): + super(OnSignalNode, self).__init__(*args, **kwargs) + array_nodes[str(id(self))] = self + + + def lnx_init(self, context): + self.add_input('LnxDynamicSocket', 'Signal') + self.add_output('LnxNodeSocketAction', 'Out') + + + def draw_buttons(self, context, layout): + row = layout.row(align=True) + op = row.operator('lnx.node_add_output', text='Add Arg', icon='PLUS', emboss=True) + op.node_index = str(id(self)) + op.socket_type = 'LnxDynamicSocket' + op.name_format = "Arg {0}" + op.index_name_offset = 0 + column = row.column(align=True) + op = column.operator('lnx.node_remove_output', text='', icon='X', emboss=True) + op.node_index = str(id(self)) + if len(self.outputs) == self.min_outputs: + column.enabled = False diff --git a/leenkx/blender/lnx/logicnode/signal/LN_signal.py b/leenkx/blender/lnx/logicnode/signal/LN_signal.py new file mode 100644 index 00000000..c0970bad --- /dev/null +++ b/leenkx/blender/lnx/logicnode/signal/LN_signal.py @@ -0,0 +1,27 @@ +from lnx.logicnode.lnx_nodes import * + + +class SignalNode(LnxLogicTreeNode): + """Creates a new Signal or references an existing Signal from an object's property. + + **Standalone Mode (default):** + Creates a new Signal instance that can be connected to OnSignal and EmitSignal nodes. + The Signal is stored in the LogicTree and persists for the lifetime of the trait. + + **Reference Mode:** + When Object and Property inputs are connected, retrieves an existing Signal + from a Haxe trait's property using reflection. + + @seeNode On Signal + @seeNode Emit Signal""" + + bl_idname = 'LNSignalNode' + bl_label = 'Signal' + lnx_version = 1 + lnx_section = 'signal' + + + def lnx_init(self, context): + self.add_input('LnxNodeSocketObject', 'Object') + self.add_input('LnxStringSocket', 'Property') + self.add_output('LnxDynamicSocket', 'Signal') diff --git a/leenkx/blender/lnx/logicnode/signal/__init__.py b/leenkx/blender/lnx/logicnode/signal/__init__.py new file mode 100644 index 00000000..87289dc3 --- /dev/null +++ b/leenkx/blender/lnx/logicnode/signal/__init__.py @@ -0,0 +1,3 @@ +from lnx.logicnode.lnx_nodes import add_node_section + +add_node_section(name='signal', category='Signal') diff --git a/leenkx/blender/lnx/logicnode/trait/LN_set_first_person_controller_settings.py b/leenkx/blender/lnx/logicnode/trait/LN_set_first_person_controller_settings.py new file mode 100644 index 00000000..8c4909d4 --- /dev/null +++ b/leenkx/blender/lnx/logicnode/trait/LN_set_first_person_controller_settings.py @@ -0,0 +1,28 @@ +from lnx.logicnode.lnx_nodes import * + +class SetFirstPersonControllerNode(LnxLogicTreeNode): + """Config Visual""" + bl_idname = 'LNSetFirstPersonControllerNode' + bl_label = 'Set FirstPersonControllerSettings' + lnx_section = 'props' + lnx_version = 1 + + def lnx_init(self, context): + self.add_input('LnxNodeSocketAction', 'In') + self.add_input('LnxNodeSocketObject', 'Object') + + """Config de la camara""" + self.add_input('LnxFloatSocket', 'RotationSpeed') + self.add_input('LnxFloatSocket', 'MaxPitch') + self.add_input('LnxFloatSocket', 'MinPitch') + + self.add_input('LnxFloatSocket', 'MoveSpeed') + self.add_input('LnxFloatSocket', 'RunSpeed') + + self.add_input('LnxBoolSocket', 'EnableJump') + self.add_input('LnxBoolSocket', 'EnableAllowAirJump') + self.add_input('LnxBoolSocket', 'EnableRun') + self.add_input('LnxBoolSocket', 'EnableStamina') + self.add_input('LnxBoolSocket', 'EnableFatigue') + + self.add_output('LnxNodeSocketAction', 'Out') diff --git a/leenkx/blender/lnx/logicnode/trait/LN_set_overhead_person_controller_settings.py b/leenkx/blender/lnx/logicnode/trait/LN_set_overhead_person_controller_settings.py new file mode 100644 index 00000000..995568a3 --- /dev/null +++ b/leenkx/blender/lnx/logicnode/trait/LN_set_overhead_person_controller_settings.py @@ -0,0 +1,28 @@ +from lnx.logicnode.lnx_nodes import * + +class SetOverheadPersonControllerNode(LnxLogicTreeNode): + """Config Visual""" + bl_idname = 'LNSetOverheadPersonControllerNode' + bl_label = 'Set OverheadPersonControllerSettings' + lnx_section = 'props' + lnx_version = 2 + + def lnx_init(self, context): + self.add_input('LnxNodeSocketAction', 'In') + + self.add_input('LnxNodeSocketObject', 'Object') + + """Suavizado""" + self.add_input('LnxBoolSocket', 'EnableSmoothTrack') + self.add_input('LnxFloatSocket', 'SmoothSpeed') + + self.add_input('LnxFloatSocket', 'MoveSpeed') + self.add_input('LnxFloatSocket', 'RunSpeed') + + self.add_input('LnxBoolSocket', 'EnableJump') + self.add_input('LnxBoolSocket', 'EnableAllowAirJump') + self.add_input('LnxBoolSocket', 'EnableRun') + self.add_input('LnxBoolSocket', 'EnableStamina') + self.add_input('LnxBoolSocket', 'EnableFatigue') + + self.add_output('LnxNodeSocketAction', 'Out') diff --git a/leenkx/blender/lnx/logicnode/transform/LN_replace_object.py b/leenkx/blender/lnx/logicnode/transform/LN_replace_object.py new file mode 100644 index 00000000..6b1cea77 --- /dev/null +++ b/leenkx/blender/lnx/logicnode/transform/LN_replace_object.py @@ -0,0 +1,15 @@ +from lnx.logicnode.lnx_nodes import * + +class ReplaceObjectNode(LnxLogicTreeNode): + """Replace location and rotation between two objects""" + bl_idname = 'LNReplaceObjectNode' + bl_label = 'Replace Object' + lnx_version = 2 + + def lnx_init(self, context): + self.add_input('LnxNodeSocketAction', 'In') + self.add_input('LnxNodeSocketObject', 'Base') + self.add_input('LnxNodeSocketObject', 'Replace') + self.add_input('LnxBoolSocket', 'Invert') + self.add_input('LnxBoolSocket', 'Scale') + self.add_output('LnxNodeSocketAction', 'Out') diff --git a/leenkx/blender/lnx/make.py b/leenkx/blender/lnx/make.py index 9a148496..99d220ec 100644 --- a/leenkx/blender/lnx/make.py +++ b/leenkx/blender/lnx/make.py @@ -118,10 +118,12 @@ def remove_readonly(func, path, excinfo): func(path) -appended_scenes = [] +linked_blend_paths = [] +linked_scenes = [] def load_external_blends(): - global appended_scenes + global linked_scenes + global linked_blend_paths wrd = bpy.data.worlds['Lnx'] if not hasattr(wrd, 'lnx_external_blends_path'): @@ -146,22 +148,24 @@ def load_external_blends(): with bpy.data.libraries.load(blend_path, link=True) as (data_from, data_to): data_to.scenes = list(data_from.scenes) + linked_blend_paths.append(blend_path) + for scn in data_to.scenes: - if scn is not None and scn not in appended_scenes: - # make name unique with file name - scn.name += "_" + filename.replace(".blend", "") - appended_scenes.append(scn) + if scn is not None and scn not in linked_scenes: + linked_scenes.append(scn) log.info(f"Loaded external blend: {blend_path}") except Exception as e: log.error(f"Failed to load external blend {blend_path}: {e}") def clear_external_scenes(): - global appended_scenes - if not appended_scenes: + global linked_blend_paths + global linked_scenes + + if not linked_scenes and not linked_blend_paths: return - for scn in appended_scenes: + for scn in linked_scenes: try: bpy.data.scenes.remove(scn, do_unlink=True) except Exception as e: @@ -169,7 +173,7 @@ def clear_external_scenes(): for lib in list(bpy.data.libraries): try: - if lib.users == 0: + if lib.users == 0 or lib.filepath in linked_blend_paths: bpy.data.libraries.remove(lib) except Exception as e: log.error(f"Failed to remove library {lib.name}: {e}") @@ -179,7 +183,8 @@ def clear_external_scenes(): except Exception as e: log.error(f"Failed to purge orphan data: {e}") - appended_scenes = [] + linked_scenes = [] + linked_blend_paths = [] def export_data(fp, sdk_path): state.is_exporting = True @@ -190,6 +195,11 @@ def export_data(fp, sdk_path): def export_data_impl(fp, sdk_path): + # Reload all libraries to retrieve updated data without needing to restart Blender + for lib in bpy.data.libraries: + lib.reload() + log.info(f"Reloaded: {lib.filepath}") + load_external_blends() wrd = bpy.data.worlds['Lnx'] @@ -263,8 +273,10 @@ def export_data_impl(fp, sdk_path): for scene in bpy.data.scenes: if scene.lnx_export: + # Reset shader comparison arrays to prevent cross-scene shader merging + assets.reset_shader_cons() ext = '.lz4' if LeenkxExporter.compress_enabled else '.lnx' - asset_path = build_dir + '/compiled/Assets/' + lnx.utils.safestr(scene.name) + ext + asset_path = build_dir + '/compiled/Assets/' + lnx.utils.safestr(scene.name + "_" + os.path.basename(scene.library.filepath).replace(".blend", "") if scene.library else scene.name) + ext LeenkxExporter.export_scene(bpy.context, asset_path, scene=scene, depsgraph=depsgraph) if LeenkxExporter.export_physics: physics_found = True diff --git a/leenkx/blender/lnx/make_renderpath.py b/leenkx/blender/lnx/make_renderpath.py index 0e09c2f2..3c92657b 100644 --- a/leenkx/blender/lnx/make_renderpath.py +++ b/leenkx/blender/lnx/make_renderpath.py @@ -149,7 +149,7 @@ def add_world_defs(): wrd.world_defs += '_Clusters' assets.add_khafile_def('lnx_clusters') - if '_Rad' in wrd.world_defs and '_Brdf' not in wrd.world_defs: + if ('_Rad' in wrd.world_defs or ('_EnvCol' in wrd.world_defs and rpdat.lnx_material_model == 'Full')) and '_Brdf' not in wrd.world_defs: wrd.world_defs += '_Brdf' assets.add_khafile_def("lnx_brdf") @@ -369,6 +369,8 @@ def build(): assets.add_khafile_def('rp_voxels={0}'.format(rpdat.rp_voxels)) assets.add_khafile_def('rp_voxelgi_resolution={0}'.format(rpdat.rp_voxelgi_resolution)) assets.add_khafile_def('rp_voxelgi_resolution_z={0}'.format(rpdat.rp_voxelgi_resolution_z)) + else: + assets.add_khafile_def('rp_voxels=Off') if rpdat.lnx_rp_resolution == 'Custom': assets.add_khafile_def('rp_resolution_filter={0}'.format(rpdat.lnx_rp_resolution_filter)) diff --git a/leenkx/blender/lnx/make_world.py b/leenkx/blender/lnx/make_world.py index 9e639a1f..9bdf0ddf 100644 --- a/leenkx/blender/lnx/make_world.py +++ b/leenkx/blender/lnx/make_world.py @@ -31,6 +31,29 @@ callback = None shader_datas = [] +def add_world_def(world: bpy.types.World, define: str): + if define not in world.world_defs: + world.world_defs += define + + +def add_global_def(define: str): + wrd = bpy.data.worlds['Lnx'] + if define not in wrd.world_defs: + wrd.world_defs += define + + +def add_irradiance_defs(world: bpy.types.World, rpdat): + if rpdat.lnx_irradiance and rpdat.lnx_material_model != 'Solid': + add_world_def(world, '_Irr') + add_global_def('_Irr') + assets.add_khafile_def("lnx_irradiance") + + +def mark_color_environment(world: bpy.types.World): + add_world_def(world, '_EnvCol') + add_global_def('_EnvCol') + + def build(): """Builds world shaders for all exported worlds.""" global shader_datas @@ -38,6 +61,10 @@ def build(): wrd = bpy.data.worlds['Lnx'] rpdat = lnx.utils.get_rp() + if rpdat is None: + log.error("No render path found. Please ensure a valid render path is selected.") + return + mobile_mat = rpdat.lnx_material_model == 'Mobile' or rpdat.lnx_material_model == 'Solid' envpath = os.path.join(lnx.utils.get_fp_build(), 'compiled', 'Assets', 'envmaps') @@ -170,7 +197,7 @@ def build_node_tree(world: bpy.types.World, frag: Shader, vert: Shader, con: Sha # film_transparent, do not render if bpy.context.scene is not None and bpy.context.scene.render.film_transparent: - world.world_defs += '_EnvCol' + mark_color_environment(world) frag.add_uniform('vec3 backgroundCol', link='_backgroundCol') frag.write('fragColor.rgb = backgroundCol;') return @@ -191,15 +218,11 @@ def build_node_tree(world: bpy.types.World, frag: Shader, vert: Shader, con: Sha # No world nodes/no output node, use background color if not is_parsed: - solid_mat = rpdat.lnx_material_model == 'Solid' - if rpdat.lnx_irradiance and not solid_mat: - world.world_defs += '_Irr' - assets.add_khafile_def("lnx_irradiance") + add_irradiance_defs(world, rpdat) col = world.color world.lnx_envtex_color = [col[0], col[1], col[2], 1.0] world.lnx_envtex_strength = 1.0 - world.world_defs += '_EnvCol' - assets.add_khafile_def("lnx_envcol") + mark_color_environment(world) # Clouds enabled if rpdat.lnx_clouds and world.lnx_use_clouds: @@ -209,12 +232,13 @@ def build_node_tree(world: bpy.types.World, frag: Shader, vert: Shader, con: Sha wrd.world_defs += '_EnvClouds' frag_write_clouds(world, frag) - if '_EnvSky' in world.world_defs or '_EnvTex' in world.world_defs or '_EnvImg' in world.world_defs or '_EnvClouds' in world.world_defs: + if '_EnvSky' in world.world_defs or '_EnvTex' in world.world_defs or '_EnvImg' in world.world_defs or '_EnvCol' in world.world_defs or '_EnvClouds' in world.world_defs: frag.add_uniform('float envmapStrength', link='_envmapStrength') # Clear background color if '_EnvCol' in world.world_defs: - frag.write('fragColor.rgb = backgroundCol;') + frag.add_uniform('vec3 backgroundCol', link='_backgroundCol') + frag.write('fragColor.rgb = backgroundCol * envmapStrength;') elif '_EnvTex' in world.world_defs and '_EnvLDR' in world.world_defs: frag.write('fragColor.rgb = pow(fragColor.rgb, vec3(2.2));') @@ -273,15 +297,11 @@ def parse_world_output(world: bpy.types.World, node_output: bpy.types.Node, frag def parse_surface(world: bpy.types.World, node_surface: bpy.types.Node, frag: Shader): - wrd = bpy.data.worlds['Lnx'] rpdat = lnx.utils.get_rp() - solid_mat = rpdat.lnx_material_model == 'Solid' if node_surface.type in ('BACKGROUND', 'EMISSION'): # Append irradiance define - if rpdat.lnx_irradiance and not solid_mat: - wrd.world_defs += '_Irr' - assets.add_khafile_def("lnx_irradiance") + add_irradiance_defs(world, rpdat) # Extract environment strength # Todo: follow/parse strength input @@ -292,12 +312,8 @@ def parse_surface(world: bpy.types.World, node_surface: bpy.types.Node, frag: Sh frag.write(f'fragColor.rgb = {out};') if not node_surface.inputs[0].is_linked: - solid_mat = rpdat.lnx_material_model == 'Solid' - if rpdat.lnx_irradiance and not solid_mat: - world.world_defs += '_Irr' - assets.add_khafile_def("lnx_irradiance") world.lnx_envtex_color = node_surface.inputs[0].default_value - world.lnx_envtex_strength = 1.0 + mark_color_environment(world) else: log.warn(f'World node type {node_surface.type} must not be connected to the world output node!') diff --git a/leenkx/blender/lnx/material/cycles.py b/leenkx/blender/lnx/material/cycles.py index a7b33b2b..12a6ac16 100644 --- a/leenkx/blender/lnx/material/cycles.py +++ b/leenkx/blender/lnx/material/cycles.py @@ -321,29 +321,18 @@ def parse_shader(node: bpy.types.Node, socket: bpy.types.NodeSocket) -> Tuple[st 'MIX_SHADER', 'ADD_SHADER', 'BSDF_PRINCIPLED', - 'PRINCIPLED_BSDF', 'BSDF_DIFFUSE', - 'DIFFUSE_BSDF', 'BSDF_GLOSSY', - 'GLOSSY_BSDF', 'BSDF_SHEEN', - 'SHEEN_BSDF', 'AMBIENT_OCCLUSION', 'BSDF_ANISOTROPIC', - 'ANISOTROPIC_BSDF', 'EMISSION', 'BSDF_GLASS', - 'GLASS_BSDF', - 'BSDF_REFRACTION', - 'REFRACTION_BSDF', 'HOLDOUT', 'SUBSURFACE_SCATTERING', 'BSDF_TRANSLUCENT', - 'TRANSLUCENT_BSDF', 'BSDF_TRANSPARENT', - 'TRANSPARENT_BSDF', 'BSDF_VELVET', - 'VELVET_BSDF', ) state.reset_outs() @@ -377,7 +366,7 @@ def parse_shader(node: bpy.types.Node, socket: bpy.types.NodeSocket) -> Tuple[st mat_state.emission_type = mat_state.EmissionType.SHADED if state.parse_opacity: state.out_opacity = parse_value_input(node.inputs[1]) - state.out_ior = 1.450; + state.out_ior = 1.450 else: return parse_group(node, socket) @@ -394,6 +383,21 @@ def parse_shader(node: bpy.types.Node, socket: bpy.types.NodeSocket) -> Tuple[st return state.get_outs() +# Use an array of socket names for compatibility across Blender versions +def get_vector_input(node: bpy.types.Node, socket_names: Tuple[str, ...]) -> vec3str: + for name in socket_names: + if name in node.inputs: + try: + return parse_vector_input(node.inputs[name]) + except Exception: + log.warn(f'Failed to parse input "{name}" on node "{node.name}"') + else: + # FIXME: Fallback to default value if the node isn't found + log.warn(f'Input "{name}" not found on node "{node.name}", returning default None') + return None + + + def parse_displacement_input(inp): if inp.is_linked: l = inp.links[0] @@ -504,6 +508,20 @@ def parse_vector(node: bpy.types.Node, socket: bpy.types.NodeSocket) -> str: return "vec3(0, 0, 0)" +# Use an array of socket names for compatibility across Blender versions +def get_value_input(node: bpy.types.Node, socket_names: Tuple[str, ...]) -> floatstr: + for name in socket_names: + if name in node.inputs: + try: + return parse_value_input(node.inputs[name]) + except Exception: + log.warn(f'Failed to parse input "{name}" on node "{node.name}"') + else: + # FIXME: Fallback to default value if the node isn't found + log.warn(f'Input "{name}" not found on node "{node.name}", returning default 1.0') + return '1.0' + + def parse_normal_map_color_input(inp, strength_input=None): frag = state.frag @@ -731,7 +749,7 @@ def store_var_name(node: bpy.types.Node) -> str: return name + '_store' -def texture_store(node, tex, tex_name, to_linear=False, tex_link=None, default_value=None, is_lnx_mat_param=None): +def texture_store(node, tex, tex_name, to_linear=False, unpremultiply=False, tex_link=None, default_value=None, is_lnx_mat_param=None): curshader = state.curshader tex_store = store_var_name(node) @@ -770,6 +788,9 @@ def texture_store(node, tex, tex_name, to_linear=False, tex_link=None, default_v else: curshader.write('vec4 {0} = texture({1}, {2}.xy);'.format(tex_store, tex_name, uv_name)) + if unpremultiply: + curshader.write('if ({0}.a > 0.0) {0}.rgb /= {0}.a;'.format(tex_store)) + if to_linear: curshader.write('{0}.rgb = pow({0}.rgb, vec3(2.2));'.format(tex_store)) diff --git a/leenkx/blender/lnx/material/cycles_functions.py b/leenkx/blender/lnx/material/cycles_functions.py index 1ba23211..f3a4a57b 100644 --- a/leenkx/blender/lnx/material/cycles_functions.py +++ b/leenkx/blender/lnx/material/cycles_functions.py @@ -1,9 +1,9 @@ str_tex_proc = """ -// -// By Morgan McGuire @morgan3d, http://graphicscodex.com -float hash_f(const float n) { return fract(sin(n) * 1e4); } -float hash_f(const vec2 p) { return fract(1e4 * sin(17.0 * p.x + p.y * 0.1) * (0.1 + abs(sin(p.y * 13.0 + p.x)))); } -float hash_f(const vec3 co){ return fract(sin(dot(co.xyz, vec3(12.9898,78.233,52.8265)) * 24.384) * 43758.5453); } +// Hash functions by Dave Hoskins +// +float hash_f(float p) { p = fract(p * 0.1031); p *= p + 33.33; p *= p + p; return fract(p); } +float hash_f(vec2 p) { vec3 p3 = fract(vec3(p.xyx) * 0.1031); p3 += dot(p3, p3.yzx + 33.33); return fract((p3.x + p3.y) * p3.z); } +float hash_f(vec3 p3) { p3 = fract(p3 * 0.1031); p3 += dot(p3, p3.zyx + 31.32); return fract((p3.x + p3.y) * p3.z); } float noise(const vec3 x) { const vec3 step = vec3(110, 241, 171); @@ -418,11 +418,12 @@ float tex_brick_blender_f(vec3 co, str_tex_wave = """ -float tex_wave_f(const vec3 p, const int type, const int profile, const float dist, const float detail, const float detail_scale) { +float tex_wave_f(const vec3 p, const int type, const int profile, const float dist, const float detail, const float detail_scale, const float phase_offset) { float n; if(type == 0) n = (p.x + p.y + p.z) * 9.5; else n = length(p) * 13.0; if(dist != 0.0) n += dist * fractal_noise(p * detail_scale, detail) * 2.0 - 1.0; + n += phase_offset; if(profile == 0) { return 0.5 + 0.5 * sin(n - PI); } else { n /= 2.0 * PI; diff --git a/leenkx/blender/lnx/material/cycles_nodes/nodes_converter.py b/leenkx/blender/lnx/material/cycles_nodes/nodes_converter.py index 7aaaab23..22566fe2 100644 --- a/leenkx/blender/lnx/material/cycles_nodes/nodes_converter.py +++ b/leenkx/blender/lnx/material/cycles_nodes/nodes_converter.py @@ -83,37 +83,28 @@ def parse_clamp(node: bpy.types.ShaderNodeClamp, out_socket: bpy.types.NodeSocke def parse_valtorgb(node: bpy.types.ShaderNodeValToRGB, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: + # Alpha (TODO: make ColorRamp calculation vec4-based and split afterwards) + if out_socket == node.outputs[1]: + return '1.0' input_fac: bpy.types.NodeSocket = node.inputs[0] - alpha_out = out_socket == node.outputs[1] + fac: str = c.parse_value_input(input_fac) if input_fac.is_linked else c.to_vec1(input_fac.default_value) interp = node.color_ramp.interpolation elems = node.color_ramp.elements - + if len(elems) == 1: - if alpha_out: - return c.to_vec1(elems[0].color[3]) # Return alpha from the color - else: - return c.to_vec3(elems[0].color) # Return RGB - - name_prefix = c.node_name(node.name).upper() - - if alpha_out: - cols_var = name_prefix + '_ALPHAS' - else: - cols_var = name_prefix + '_COLS' + return c.to_vec3(elems[0].color) + + # Write color array + # The last entry is included twice so that the interpolation + # between indices works (no out of bounds error) + cols_var = c.node_name(node.name).upper() + '_COLS' if state.current_pass == ParserPass.REGULAR: - if alpha_out: - cols_entries = ', '.join(f'{elem.color[3]}' for elem in elems) - # Add last value twice to avoid out of bounds access - cols_entries += f', {elems[len(elems) - 1].color[3]}' - state.curshader.add_const("float", cols_var, cols_entries, array_size=len(elems) + 1) - else: - # Create array of RGB values for color output - cols_entries = ', '.join(f'vec3({elem.color[0]}, {elem.color[1]}, {elem.color[2]})' for elem in elems) - cols_entries += f', vec3({elems[len(elems) - 1].color[0]}, {elems[len(elems) - 1].color[1]}, {elems[len(elems) - 1].color[2]})' - state.curshader.add_const("vec3", cols_var, cols_entries, array_size=len(elems) + 1) + cols_entries = ', '.join(f'vec3({elem.color[0]}, {elem.color[1]}, {elem.color[2]})' for elem in elems) + cols_entries += f', vec3({elems[len(elems) - 1].color[0]}, {elems[len(elems) - 1].color[1]}, {elems[len(elems) - 1].color[2]})' + state.curshader.add_const("vec3", cols_var, cols_entries, array_size=len(elems) + 1) fac_var = c.node_name(node.name) + '_fac' + state.get_parser_pass_suffix() state.curshader.write(f'float {fac_var} = {fac};') @@ -131,22 +122,22 @@ def parse_valtorgb(node: bpy.types.ShaderNodeValToRGB, out_socket: bpy.types.Nod # Linear interpolation else: - # Write factor array - same for both color and alpha - facs_var = name_prefix + '_FACS' + # Write factor array + facs_var = c.node_name(node.name).upper() + '_FACS' if state.current_pass == ParserPass.REGULAR: facs_entries = ', '.join(str(elem.position) for elem in elems) - # Add one more entry at the rightmost position to avoid out of bounds access + # Add one more entry at the rightmost position so that the + # interpolation between indices works (no out of bounds error) facs_entries += ', 1.0' state.curshader.add_const("float", facs_var, facs_entries, array_size=len(elems) + 1) - # Calculation for interpolation position + # Mix color prev_stop_fac = f'{facs_var}[{index_var}]' next_stop_fac = f'{facs_var}[{index_var} + 1]' prev_stop_col = f'{cols_var}[{index_var}]' next_stop_col = f'{cols_var}[{index_var} + 1]' rel_pos = f'({fac_var} - {prev_stop_fac}) * (1.0 / ({next_stop_fac} - {prev_stop_fac}))' - - # Use mix function for both alpha and color outputs (mix works on floats too) + return f'mix({prev_stop_col}, {next_stop_col}, max({rel_pos}, 0.0))' if bpy.app.version > (3, 2, 0): diff --git a/leenkx/blender/lnx/material/cycles_nodes/nodes_input.py b/leenkx/blender/lnx/material/cycles_nodes/nodes_input.py index 6780a86e..c37c611e 100644 --- a/leenkx/blender/lnx/material/cycles_nodes/nodes_input.py +++ b/leenkx/blender/lnx/material/cycles_nodes/nodes_input.py @@ -248,7 +248,7 @@ def parse_objectinfo(node: bpy.types.ShaderNodeObjectInfo, out_socket: bpy.types def parse_particleinfo(node: bpy.types.ShaderNodeParticleInfo, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: - particles_on = lnx.utils.get_rp().lnx_particles == 'On' + particles_on = lnx.utils.get_rp().lnx_particles == 'GPU' # Index if out_socket == node.outputs[0]: @@ -310,27 +310,23 @@ def parse_texcoord(node: bpy.types.ShaderNodeTexCoord, out_socket: bpy.types.Nod return 'vec3(0.0)' state.con.add_elem('tex', 'short2norm') state.dxdy_varying_input_value = True + return 'vec3(texCoord.x, 1.0 - texCoord.y, 0.0)' elif out_socket == node.outputs[3]: # Object state.dxdy_varying_input_value = True return 'mposition' elif out_socket == node.outputs[4]: # Camera - state.curshader.add_uniform('mat4 V', link='_viewMatrix') - if not state.frag.contains('vec3 viewPosition;'): - state.frag.write_init('vec3 viewPosition = (V * vec4(wposition, 1.0)).xyz;') - state.dxdy_varying_input_value = True - return 'viewPosition' + return 'vec3(0.0)' # 'vposition' elif out_socket == node.outputs[5]: # Window # TODO: Don't use gl_FragCoord here, it uses different axes on different graphics APIs state.frag.add_uniform('vec2 screenSize', link='_screenSize') state.dxdy_varying_input_value = True return f'vec3(gl_FragCoord.xy / screenSize, 0.0)' elif out_socket == node.outputs[6]: # Reflection - state.curshader.add_uniform('vec3 eye', link='_cameraPosition') - if not state.frag.contains('vec3 reflectionVector;'): - state.frag.write_init('vec3 reflectionVector = reflect(normalize(wposition - eye), normalize(n));') - state.dxdy_varying_input_value = True - return 'reflectionVector' + if state.context == ParserContext.WORLD: + state.dxdy_varying_input_value = True + return 'n' + return 'vec3(0.0)' def parse_uvmap(node: bpy.types.ShaderNodeUVMap, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: diff --git a/leenkx/blender/lnx/material/cycles_nodes/nodes_shader.py b/leenkx/blender/lnx/material/cycles_nodes/nodes_shader.py index 6cb8aab7..6f8cea4c 100644 --- a/leenkx/blender/lnx/material/cycles_nodes/nodes_shader.py +++ b/leenkx/blender/lnx/material/cycles_nodes/nodes_shader.py @@ -85,6 +85,8 @@ def parse_addshader(node: bpy.types.ShaderNodeAddShader, out_socket: NodeSocket, state.out_opacity = '({0} * 0.5 + {1} * 0.5)'.format(opac1, opac2) state.out_ior = '({0} * 0.5 + {1} * 0.5)'.format(ior1, ior2) +# TODO: Refactor using c.get_*_input() + if bpy.app.version < (2, 92, 0): def parse_bsdfprincipled(node: bpy.types.ShaderNodeBsdfPrincipled, out_socket: NodeSocket, state: ParserState) -> None: @@ -224,12 +226,6 @@ if bpy.app.version < (4, 1, 0): c.write_normal(node.inputs[2]) state.out_basecol = c.parse_vector_input(node.inputs[0]) state.out_roughness = c.parse_value_input(node.inputs[1]) - # Prevent black material when metal = 1.0 and roughness = 0.0 - try: - if float(state.out_roughness) < 0.00101: - state.out_roughness = '0.001' - except ValueError: - pass state.out_metallic = '1.0' else: def parse_bsdfglossy(node: bpy.types.ShaderNodeBsdfAnisotropic, out_socket: NodeSocket, state: ParserState) -> None: @@ -237,12 +233,6 @@ else: c.write_normal(node.inputs[4]) state.out_basecol = c.parse_vector_input(node.inputs[0]) state.out_roughness = c.parse_value_input(node.inputs[1]) - # Prevent black material when metal = 1.0 and roughness = 0.0 - try: - if float(state.out_roughness) < 0.00101: - state.out_roughness = '0.001' - except ValueError: - pass state.out_metallic = '1.0' diff --git a/leenkx/blender/lnx/material/cycles_nodes/nodes_texture.py b/leenkx/blender/lnx/material/cycles_nodes/nodes_texture.py index 70f32d19..daa8e20b 100644 --- a/leenkx/blender/lnx/material/cycles_nodes/nodes_texture.py +++ b/leenkx/blender/lnx/material/cycles_nodes/nodes_texture.py @@ -28,12 +28,11 @@ if lnx.is_reload(__name__): else: lnx.enable_reload(__name__) - def parse_tex_brick(node: bpy.types.ShaderNodeTexBrick, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: state.curshader.add_function(c_functions.str_tex_brick_blender) - if node.inputs[0].is_linked: - co = c.parse_vector_input(node.inputs[0]) + if node.inputs['Vector'].is_linked: + co = c.get_vector_input(node, ['Vector']) else: co = 'bposition' @@ -42,23 +41,23 @@ def parse_tex_brick(node: bpy.types.ShaderNodeTexBrick, out_socket: bpy.types.No squash_amount = node.squash squash_frequency = node.squash_frequency - col1 = c.parse_vector_input(node.inputs[1]) - col2 = c.parse_vector_input(node.inputs[2]) - col3 = c.parse_vector_input(node.inputs[3]) - scale = c.parse_value_input(node.inputs[4]) - mortar_size = c.parse_value_input(node.inputs[5]) - mortar_smooth = c.parse_value_input(node.inputs[6]) - bias = c.parse_value_input(node.inputs[7]) - brick_width = c.parse_value_input(node.inputs[8]) - row_height = c.parse_value_input(node.inputs[9]) - #res = f'tex_brick({co} * {scale}, {col1}, {col2}, {col3})' + col1 = c.get_vector_input(node, ['Color1']) + col2 = c.get_vector_input(node, ['Color2']) + mortar = c.get_vector_input(node, ['Mortar']) + scale = c.get_value_input(node, ['Scale']) + mortar_size = c.get_value_input(node, ['Mortar Size']) + mortar_smooth = c.get_value_input(node, ['Mortar Smooth']) + bias = c.get_value_input(node, ['Bias']) + brick_width = c.get_value_input(node, ['Brick Width']) + row_height = c.get_value_input(node, ['Row Height']) + #res = f'tex_brick({co} * {scale}, {col1}, {col2}, {mortar})' # Color - if out_socket == node.outputs[0]: - res = f'tex_brick_blender({co}, {col1}, {col2}, {col3}, {scale}, {mortar_size}, {mortar_smooth}, {bias}, {brick_width}, {row_height}, {offset_amount}, {offset_frequency}, {squash_amount}, {squash_frequency})' + if out_socket == node.outputs['Color']: + res = f'tex_brick_blender({co}, {col1}, {col2}, {mortar}, {scale}, {mortar_size}, {mortar_smooth}, {bias}, {brick_width}, {row_height}, {offset_amount}, {offset_frequency}, {squash_amount}, {squash_frequency})' # Fac else: - res = f'tex_brick_blender_f({co}, {col1}, {col2}, {col3}, {scale}, {mortar_size}, {mortar_smooth}, {bias}, {brick_width}, {row_height}, {offset_amount}, {offset_frequency}, {squash_amount}, {squash_frequency})' + res = f'tex_brick_blender_f({co}, {col1}, {col2}, {mortar}, {scale}, {mortar_size}, {mortar_smooth}, {bias}, {brick_width}, {row_height}, {offset_amount}, {offset_frequency}, {squash_amount}, {squash_frequency})' return res @@ -66,28 +65,28 @@ def parse_tex_brick(node: bpy.types.ShaderNodeTexBrick, out_socket: bpy.types.No def parse_tex_checker(node: bpy.types.ShaderNodeTexChecker, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: state.curshader.add_function(c_functions.str_tex_checker) - if node.inputs[0].is_linked: - co = c.parse_vector_input(node.inputs[0]) + if node.inputs['Vector'].is_linked: + co = c.get_vector_input(node, ['Vector']) else: co = 'bposition' + scale = c.get_value_input(node, ['Scale']) + # Color - if out_socket == node.outputs[0]: - col1 = c.parse_vector_input(node.inputs[1]) - col2 = c.parse_vector_input(node.inputs[2]) - scale = c.parse_value_input(node.inputs[3]) + if out_socket == node.outputs['Color']: + col1 = c.get_vector_input(node, ['Color1']) + col2 = c.get_vector_input(node, ['Color2']) res = f'tex_checker({co}, {col1}, {col2}, {scale})' # Fac else: - scale = c.parse_value_input(node.inputs[3]) res = 'tex_checker_f({0}, {1})'.format(co, scale) return res def parse_tex_gradient(node: bpy.types.ShaderNodeTexGradient, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: - if node.inputs[0].is_linked: - co = c.parse_vector_input(node.inputs[0]) + if node.inputs['Vector'].is_linked: + co = c.get_vector_input(node, ['Vector']) else: co = 'bposition' @@ -108,7 +107,7 @@ def parse_tex_gradient(node: bpy.types.ShaderNodeTexGradient, out_socket: bpy.ty f = f'max(1.0 - sqrt({co}.x * {co}.x + {co}.y * {co}.y + {co}.z * {co}.z), 0.0)' # Color - if out_socket == node.outputs[0]: + if out_socket == node.outputs['Color']: res = f'vec3(clamp({f}, 0.0, 1.0))' # Fac else: @@ -119,7 +118,7 @@ def parse_tex_gradient(node: bpy.types.ShaderNodeTexGradient, out_socket: bpy.ty def parse_tex_image(node: bpy.types.ShaderNodeTexImage, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: # Color or Alpha output - use_color_out = out_socket == node.outputs[0] + use_color_out = out_socket == node.outputs['Color'] if state.context == ParserContext.OBJECT: tex_store = c.store_var_name(node) @@ -147,11 +146,12 @@ def parse_tex_image(node: bpy.types.ShaderNodeTexImage, out_socket: bpy.types.No state.curshader.write_textures += 1 if node.lnx_material_param and tex['file'] is not None: tex_default_file = tex['file'] + unpremultiply = node.image is not None and node.image.alpha_mode != 'CHANNEL_PACKED' if use_color_out: to_linear = node.image is not None and node.image.colorspace_settings.name == 'sRGB' - res = f'{c.texture_store(node, tex, tex_name, to_linear, tex_link=tex_link, default_value=tex_default_file, is_lnx_mat_param=is_lnx_mat_param)}.rgb' + res = f'{c.texture_store(node, tex, tex_name, to_linear, unpremultiply, tex_link=tex_link, default_value=tex_default_file, is_lnx_mat_param=is_lnx_mat_param)}.rgb' else: - res = f'{c.texture_store(node, tex, tex_name, tex_link=tex_link, default_value=tex_default_file, is_lnx_mat_param=is_lnx_mat_param)}.a' + res = f'{c.texture_store(node, tex, tex_name, unpremultiply, tex_link=tex_link, default_value=tex_default_file, is_lnx_mat_param=is_lnx_mat_param)}.a' state.curshader.write_textures -= 1 return res @@ -162,8 +162,8 @@ def parse_tex_image(node: bpy.types.ShaderNodeTexImage, out_socket: bpy.types.No 'file': '' } if use_color_out: - return '{0}.rgb'.format(c.texture_store(node, tex, tex_name, to_linear=False, tex_link=tex_link, is_lnx_mat_param=is_lnx_mat_param)) - return '{0}.a'.format(c.texture_store(node, tex, tex_name, to_linear=True, tex_link=tex_link, is_lnx_mat_param=is_lnx_mat_param)) + return '{0}.rgb'.format(c.texture_store(node, tex, tex_name, to_linear=False, unpremultiply=False, tex_link=tex_link, is_lnx_mat_param=is_lnx_mat_param)) + return '{0}.a'.format(c.texture_store(node, tex, tex_name, to_linear=True, unpremultiply=False, tex_link=tex_link, is_lnx_mat_param=is_lnx_mat_param)) # Pink color for missing texture else: @@ -240,15 +240,15 @@ def parse_tex_image(node: bpy.types.ShaderNodeTexImage, out_socket: bpy.types.No def parse_tex_magic(node: bpy.types.ShaderNodeTexMagic, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: state.curshader.add_function(c_functions.str_tex_magic) - if node.inputs[0].is_linked: - co = c.parse_vector_input(node.inputs[0]) + if node.inputs['Vector'].is_linked: + co = c.get_vector_input(node, ['Vector']) else: co = 'bposition' - scale = c.parse_value_input(node.inputs[1]) + scale = c.get_value_input(node, ['Scale']) # Color - if out_socket == node.outputs[0]: + if out_socket == node.outputs['Color']: res = f'tex_magic({co} * {scale} * 4.0)' # Fac else: @@ -260,17 +260,17 @@ if bpy.app.version < (4, 1, 0): def parse_tex_musgrave(node: bpy.types.ShaderNodeTexMusgrave, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: state.curshader.add_function(c_functions.str_tex_musgrave) - if node.inputs[0].is_linked: - co = c.parse_vector_input(node.inputs[0]) + if node.inputs['Vector'].is_linked: + co = c.get_vector_input(node, ['Vector']) else: co = 'bposition' - - scale = c.parse_value_input(node.inputs['Scale']) - detail = c.parse_value_input(node.inputs[3]) - distortion = c.parse_value_input(node.inputs[4]) - res = f'tex_musgrave_f({co} * {scale} * 0.5, {detail}, {distortion})' - + scale = c.get_value_input(node, ['Scale']) + detail = c.get_value_input(node, ['Detail']) + dimension = c.get_value_input(node, ['Dimension']) + + res = f'tex_musgrave_f({co} * {scale} * 0.5, {detail}, {dimension})' # FIXME: a `distortion` is applied instead of a `dimension` + return res @@ -280,28 +280,30 @@ def parse_tex_noise(node: bpy.types.ShaderNodeTexNoise, out_socket: bpy.types.No c.assets_add(os.path.join(lnx.utils.get_sdk_path(), 'leenkx', 'Assets', 'noise256.png')) c.assets_add_embedded_data('noise256.png') state.curshader.add_uniform('sampler2D snoise256', link='$noise256.png') - if node.inputs[0].is_linked: - co = c.parse_vector_input(node.inputs[0]) + if node.inputs['Vector'].is_linked: + co = c.get_vector_input(node, ['Vector']) else: co = 'bposition' - scale = c.parse_value_input(node.inputs[2]) - detail = c.parse_value_input(node.inputs[3]) - roughness = c.parse_value_input(node.inputs[4]) - distortion = c.parse_value_input(node.inputs[5]) + scale = c.get_value_input(node, ['Scale']) + detail = c.get_value_input(node, ['Detail']) + roughness = c.get_value_input(node, ['Roughness']) + distortion = c.get_value_input(node, ['Distortion']) if bpy.app.version >= (4, 1, 0): if node.noise_type == "FBM": state.curshader.add_function(c_functions.str_tex_musgrave) - if out_socket == node.outputs[1]: + if out_socket == node.outputs['Color']: res = 'vec3(tex_musgrave_f({0} * {1}, {2}, {3}), tex_musgrave_f({0} * {1} + 120.0, {2}, {3}), tex_musgrave_f({0} * {1} + 168.0, {2}, {3}))'.format(co, scale, detail, distortion) else: res = f'tex_musgrave_f({co} * {scale} * 1.0, {detail}, {distortion})' else: - if out_socket == node.outputs[1]: + if out_socket == node.outputs['Color']: res = 'vec3(tex_noise({0} * {1},{2},{3}), tex_noise({0} * {1} + 120.0,{2},{3}), tex_noise({0} * {1} + 168.0,{2},{3}))'.format(co, scale, detail, distortion) else: res = 'tex_noise({0} * {1},{2},{3})'.format(co, scale, detail, distortion) + if node.normalize: + res = f'(1.0 - ({res}))' else: - if out_socket == node.outputs[1]: + if out_socket == node.outputs['Color']: res = 'vec3(tex_noise({0} * {1},{2},{3}), tex_noise({0} * {1} + 120.0,{2},{3}), tex_noise({0} * {1} + 168.0,{2},{3}))'.format(co, scale, detail, distortion) else: res = 'tex_noise({0} * {1},{2},{3})'.format(co, scale, detail, distortion) @@ -311,7 +313,7 @@ if bpy.app.version < (5, 0, 0): def parse_tex_pointdensity(node: bpy.types.ShaderNodeTexPointDensity, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: # Pass through # Color - if out_socket == node.outputs[0]: + if out_socket == node.outputs['Color']: return c.to_vec3([0.0, 0.0, 0.0]) # Density else: @@ -546,21 +548,24 @@ def parse_tex_voronoi(node: bpy.types.ShaderNodeTexVoronoi, out_socket: bpy.type m = 2 elif node.distance == 'MINKOWSKI': m = 3 + # TODO: Add node.distance == 'MANHATHAN' + # Add node.feature + # Add node.voronoi_dimensions c.write_procedurals() state.curshader.add_function(c_functions.str_tex_voronoi) - if node.inputs[0].is_linked: - co = c.parse_vector_input(node.inputs[0]) + if node.inputs['Vector'].is_linked: + co = c.get_vector_input(node, ['Vector']) else: co = 'bposition' - scale = c.parse_value_input(node.inputs[2]) - exp = c.parse_value_input(node.inputs[4]) - randomness = c.parse_value_input(node.inputs[5]) + scale = c.get_value_input(node, ['Scale']) + exp = c.get_value_input(node, ['Exponent']) + randomness = c.get_value_input(node, ['Randomness']) # Color or Position - if out_socket == node.outputs[1] or out_socket == node.outputs[2]: + if out_socket == node.outputs['Color'] or out_socket == node.outputs['Position']: res = 'tex_voronoi({0}, {1}, {2}, {3}, {4}, {5})'.format(co, randomness, m, outp, scale, exp) # Distance else: @@ -572,14 +577,16 @@ def parse_tex_voronoi(node: bpy.types.ShaderNodeTexVoronoi, out_socket: bpy.type def parse_tex_wave(node: bpy.types.ShaderNodeTexWave, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: c.write_procedurals() state.curshader.add_function(c_functions.str_tex_wave) - if node.inputs[0].is_linked: - co = c.parse_vector_input(node.inputs[0]) + if node.inputs['Vector'].is_linked: + co = c.get_vector_input(node, ['Vector']) else: co = 'bposition' - scale = c.parse_value_input(node.inputs[1]) - distortion = c.parse_value_input(node.inputs[2]) - detail = c.parse_value_input(node.inputs[3]) - detail_scale = c.parse_value_input(node.inputs[4]) + scale = c.get_value_input(node, ['Scale']) + distortion = c.get_value_input(node, ['Distortion']) + detail = c.get_value_input(node, ['Detail']) + detail_scale = c.get_value_input(node, ['Detail Scale']) + phase_offset = c.get_value_input(node, ['Phase Offset']) + if node.wave_profile == 'SIN': wave_profile = 0 else: @@ -590,10 +597,10 @@ def parse_tex_wave(node: bpy.types.ShaderNodeTexWave, out_socket: bpy.types.Node wave_type = 1 # Color - if out_socket == node.outputs[0]: - res = 'vec3(tex_wave_f({0} * {1},{2},{3},{4},{5},{6}))'.format(co, scale, wave_type, wave_profile, distortion, detail, detail_scale) + if out_socket == node.outputs['Color']: + res = 'vec3(tex_wave_f({0} * {1},{2},{3},{4},{5},{6},{7}))'.format(co, scale, wave_type, wave_profile, distortion, detail, detail_scale, phase_offset) # Fac else: - res = 'tex_wave_f({0} * {1},{2},{3},{4},{5},{6})'.format(co, scale, wave_type, wave_profile, distortion, detail, detail_scale) + res = 'tex_wave_f({0} * {1},{2},{3},{4},{5},{6},{7})'.format(co, scale, wave_type, wave_profile, distortion, detail, detail_scale, phase_offset) return res diff --git a/leenkx/blender/lnx/material/cycles_nodes/nodes_vector.py b/leenkx/blender/lnx/material/cycles_nodes/nodes_vector.py index 94ec7dc1..bef69980 100644 --- a/leenkx/blender/lnx/material/cycles_nodes/nodes_vector.py +++ b/leenkx/blender/lnx/material/cycles_nodes/nodes_vector.py @@ -25,8 +25,8 @@ else: def parse_curvevec(node: bpy.types.ShaderNodeVectorCurve, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: - fac = c.parse_value_input(node.inputs[0]) - vec = c.parse_vector_input(node.inputs[1]) + fac = c.get_value_input(node, ['Fac']) + vec = c.get_vector_input(node, ['Vector']) curves = node.mapping.curves name = c.node_name(node.name) # mapping.curves[0].points[0].handle_type # bezier curve @@ -42,19 +42,17 @@ def parse_bump(node: bpy.types.ShaderNodeBump, out_socket: bpy.types.NodeSocket, return 'vec3(0.0)' # Interpolation strength - strength = c.parse_value_input(node.inputs[0]) - # Height multiplier - # distance = c.parse_value_input(node.inputs[1]) - height = c.parse_value_input(node.inputs[2]) + strength = c.get_value_input(node, ['Strength']) + # distance = c.get_value_input(node, ['Distance']) + height = c.get_value_input(node, ['Height']) + # normal = c.get_vector_input(node, ['Normal']) state.current_pass = ParserPass.DX_SCREEN_SPACE - height_dx = c.parse_value_input(node.inputs[2]) + height_dx = c.get_value_input(node, ['Height']) state.current_pass = ParserPass.DY_SCREEN_SPACE - height_dy = c.parse_value_input(node.inputs[2]) + height_dy = c.get_value_input(node, ['Height']) state.current_pass = ParserPass.REGULAR - # nor = c.parse_vector_input(node.inputs[3]) - if height_dx != height or height_dy != height: tangent = f'{c.dfdx_fine("wposition")} + n * ({height_dx} - {height})' bitangent = f'{c.dfdy_fine("wposition")} + n * ({height_dy} - {height})' @@ -79,11 +77,12 @@ def parse_bump(node: bpy.types.ShaderNodeBump, out_socket: bpy.types.NodeSocket, def parse_mapping(node: bpy.types.ShaderNodeMapping, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: + # TODO: Add support for "Normal" type # Only "Point", "Texture" and "Vector" types supported for now.. # More information about the order of operations for this node: # https://docs.blender.org/manual/en/latest/render/shader_nodes/vector/mapping.html#properties - input_vector: bpy.types.NodeSocket = node.inputs[0] + input_vector: bpy.types.NodeSocket = node.inputs['Vector'] input_location: bpy.types.NodeSocket = node.inputs['Location'] input_rotation: bpy.types.NodeSocket = node.inputs['Rotation'] input_scale: bpy.types.NodeSocket = node.inputs['Scale'] @@ -145,44 +144,48 @@ def parse_normal(node: bpy.types.ShaderNodeNormal, out_socket: bpy.types.NodeSoc return nor1 elif out_socket == node.outputs['Dot']: - nor2 = c.parse_vector_input(node.inputs["Normal"]) + nor2 = c.get_vector_input(node, ["Normal"]) return f'dot({nor1}, {nor2})' def parse_normalmap(node: bpy.types.ShaderNodeNormalMap, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: if state.curshader == state.tese: - return c.parse_vector_input(node.inputs[1]) + return c.get_vector_input(node, ["Normal"]) else: + # TODO: # space = node.space # map = node.uv_map # Color - c.parse_normal_map_color_input(node.inputs[1], node.inputs[0]) + c.parse_normal_map_color_input(node.inputs['Color'], node.inputs['Strength']) return 'n' def parse_vectortransform(node: bpy.types.ShaderNodeVectorTransform, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: - # type = node.vector_type + # TODO: + # vector_type = node.vector_type # conv_from = node.convert_from # conv_to = node.convert_to # Pass through - return c.parse_vector_input(node.inputs[0]) + return c.get_vector_input(node, ['Vector']) def parse_displacement(node: bpy.types.ShaderNodeDisplacement, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: - height = c.parse_value_input(node.inputs[0]) - midlevel = c.parse_value_input(node.inputs[1]) - scale = c.parse_value_input(node.inputs[2]) - nor = c.parse_vector_input(node.inputs[3]) + # TODO: + # space = node.space + height = c.get_value_input(node, ['Height']) + midlevel = c.get_value_input(node, ['Midlevel']) + scale = c.get_value_input(node, ['Scale']) + nor = c.get_vector_input(node, ['Normal']) return f'(vec3({height}) * {scale})' def parse_vectorrotate(node: bpy.types.ShaderNodeVectorRotate, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: type = node.rotation_type - input_vector: bpy.types.NodeSocket = c.parse_vector_input(node.inputs[0]) - input_center: bpy.types.NodeSocket = c.parse_vector_input(node.inputs[1]) - input_axis: bpy.types.NodeSocket = c.parse_vector_input(node.inputs[2]) - input_angle: bpy.types.NodeSocket = c.parse_value_input(node.inputs[3]) - input_rotation: bpy.types.NodeSocket = c.parse_vector_input(node.inputs[4]) + input_vector: bpy.types.NodeSocket = c.get_vector_input(node, ['Vector']) + input_center: bpy.types.NodeSocket = c.get_vector_input(node, ['Center']) + input_axis: bpy.types.NodeSocket = c.get_vector_input(node, ['Axis']) + input_angle: bpy.types.NodeSocket = c.get_value_input(node, ['Angle']) + input_rotation: bpy.types.NodeSocket = c.get_vector_input(node, ['Rotation']) if node.invert: input_invert = "0" diff --git a/leenkx/blender/lnx/material/make_attrib.py b/leenkx/blender/lnx/material/make_attrib.py index 4661b4d0..63c44a16 100644 --- a/leenkx/blender/lnx/material/make_attrib.py +++ b/leenkx/blender/lnx/material/make_attrib.py @@ -1,100 +1,212 @@ from typing import Optional + + import lnx.material.cycles as cycles + import lnx.material.mat_state as mat_state + import lnx.material.make_skin as make_skin + import lnx.material.make_particle as make_particle + import lnx.material.make_inst as make_inst + import lnx.material.make_tess as make_tess + import lnx.material.make_morph_target as make_morph_target + from lnx.material.shader import Shader, ShaderContext + import lnx.utils + + if lnx.is_reload(__name__): + cycles = lnx.reload_module(cycles) + mat_state = lnx.reload_module(mat_state) + make_skin = lnx.reload_module(make_skin) + make_particle = lnx.reload_module(make_particle) + make_inst = lnx.reload_module(make_inst) + make_tess = lnx.reload_module(make_tess) + make_morph_target = lnx.reload_module(make_morph_target) + lnx.material.shader = lnx.reload_module(lnx.material.shader) + from lnx.material.shader import Shader, ShaderContext + lnx.utils = lnx.reload_module(lnx.utils) + else: + lnx.enable_reload(__name__) + + + def write_vertpos(vert): + billboard = mat_state.material.lnx_billboard + particle = mat_state.material.lnx_particle_flag + # Particles + if particle: - if lnx.utils.get_rp().lnx_particles == 'On': + + if lnx.utils.get_rp().lnx_particles == 'GPU': + make_particle.write(vert, particle_info=cycles.particle_info) + # Billboards + if billboard == 'spherical': + vert.add_uniform('mat4 WV', '_worldViewMatrix') + vert.add_uniform('mat4 P', '_projectionMatrix') + vert.write('gl_Position = P * (WV * vec4(0.0, 0.0, spos.z, 1.0) + vec4(spos.x, spos.y, 0.0, 0.0));') + else: + vert.add_uniform('mat4 WVP', '_worldViewProjectionMatrix') + vert.write('gl_Position = WVP * spos;') + else: + # Billboards + if billboard == 'spherical': + vert.add_uniform('mat4 WVP', '_worldViewProjectionMatrixSphere') + elif billboard == 'cylindrical': + vert.add_uniform('mat4 WVP', '_worldViewProjectionMatrixCylinder') + else: # off + vert.add_uniform('mat4 WVP', '_worldViewProjectionMatrix') + vert.write('gl_Position = WVP * spos;') + + + def write_norpos(con_mesh: ShaderContext, vert: Shader, declare=False, write_nor=True): + is_bone = con_mesh.is_elem('bone') + is_morph = con_mesh.is_elem('morph') + if is_morph: + make_morph_target.morph_pos(vert) + if is_bone: + make_skin.skin_pos(vert) + if write_nor: + prep = 'vec3 ' if declare else '' + if is_morph: + make_morph_target.morph_nor(vert, is_bone, prep) + if is_bone: + make_skin.skin_nor(vert, is_morph, prep) + if not is_morph and not is_bone: + vert.write_attrib(prep + 'wnormal = normalize(N * vec3(nor.xy, pos.w));') + if con_mesh.is_elem('ipos'): + make_inst.inst_pos(con_mesh, vert) + + + def write_tex_coords(con_mesh: ShaderContext, vert: Shader, frag: Shader, tese: Optional[Shader]): + rpdat = lnx.utils.get_rp() + + if con_mesh.is_elem('tex'): + vert.add_out('vec2 texCoord') + vert.add_uniform('float texUnpack', link='_texUnpack') + if mat_state.material.lnx_tilesheet_flag: + if mat_state.material.lnx_particle_flag and rpdat.lnx_particles == 'On': + make_particle.write_tilesheet(vert) + else: + vert.add_uniform('vec2 tilesheetOffset', '_tilesheetOffset') - vert.write_attrib('texCoord = tex * texUnpack + tilesheetOffset;') + + vert.add_uniform('vec2 tilesheetFlip', '_tilesheetFlip') + + vert.add_uniform('vec2 tilesheetTiles', '_tilesheetTiles') + + vert.write_attrib('vec2 tileSize = vec2(1.0 / tilesheetTiles.x, 1.0 / tilesheetTiles.y);') + + vert.write_attrib('vec2 tileUV = tex * texUnpack;') + + vert.write_attrib('tileUV.x = mix(tileUV.x, tileSize.x - tileUV.x, tilesheetFlip.x);') + + vert.write_attrib('tileUV.y = mix(tileUV.y, tileSize.y - tileUV.y, tilesheetFlip.y);') + + vert.write_attrib('texCoord = tileUV + tilesheetOffset;') + else: + vert.write_attrib('texCoord = tex * texUnpack;') + + if tese is not None: + tese.write_pre = True + make_tess.interpolate(tese, 'texCoord', 2, declare_out=frag.contains('texCoord')) + tese.write_pre = False + + if con_mesh.is_elem('tex1'): + vert.add_out('vec2 texCoord1') + vert.add_uniform('float texUnpack', link='_texUnpack') + vert.write_attrib('texCoord1 = tex1 * texUnpack;') + if tese is not None: + tese.write_pre = True + make_tess.interpolate(tese, 'texCoord1', 2, declare_out=frag.contains('texCoord1')) + tese.write_pre = False + diff --git a/leenkx/blender/lnx/material/make_depth.py b/leenkx/blender/lnx/material/make_depth.py index d5c5774b..95430245 100644 --- a/leenkx/blender/lnx/material/make_depth.py +++ b/leenkx/blender/lnx/material/make_depth.py @@ -112,7 +112,7 @@ def make(context_id, rpasses, shadowmap=False, shadowmap_transparent=False): make_inst.inst_pos(con_depth, vert) rpdat = lnx.utils.get_rp() - if mat_state.material.lnx_particle_flag and rpdat.lnx_particles == 'On': + if mat_state.material.lnx_particle_flag and rpdat.lnx_particles == 'GPU': make_particle.write(vert, shadowmap=shadowmap) if is_disp: diff --git a/leenkx/blender/lnx/material/make_mesh.py b/leenkx/blender/lnx/material/make_mesh.py index 3d62747b..cd92fc04 100644 --- a/leenkx/blender/lnx/material/make_mesh.py +++ b/leenkx/blender/lnx/material/make_mesh.py @@ -58,6 +58,7 @@ def make(context_id, rpasses): con['alpha_blend_destination'] = mat.lnx_blending_destination_alpha con['alpha_blend_operation'] = mat.lnx_blending_operation_alpha con['depth_write'] = False + con['compare_mode'] = mat.lnx_compare_mode elif particle: pass # Depth prepass was performed, exclude mat with depth read that @@ -500,7 +501,13 @@ def make_forward_solid(con_mesh): vert.add_uniform('float texUnpack', link='_texUnpack') if mat_state.material.lnx_tilesheet_flag: vert.add_uniform('vec2 tilesheetOffset', '_tilesheetOffset') - vert.write('texCoord = tex * texUnpack + tilesheetOffset;') + vert.add_uniform('vec2 tilesheetFlip', '_tilesheetFlip') + vert.add_uniform('vec2 tilesheetTiles', '_tilesheetTiles') + vert.write('vec2 tileSize = vec2(1.0 / tilesheetTiles.x, 1.0 / tilesheetTiles.y);') + vert.write('vec2 tileUV = tex * texUnpack;') + vert.write('tileUV.x = mix(tileUV.x, tileSize.x - tileUV.x, tilesheetFlip.x);') + vert.write('tileUV.y = mix(tileUV.y, tileSize.y - tileUV.y, tilesheetFlip.y);') + vert.write('texCoord = tileUV + tilesheetOffset;') else: vert.write('texCoord = tex * texUnpack;') @@ -571,7 +578,7 @@ def make_forward(con_mesh): frag.write('fragColor[0].rgb = tonemapFilmic(fragColor[0].rgb);') # Particle opacity - if mat_state.material.lnx_particle_flag and lnx.utils.get_rp().lnx_particles == 'On' and mat_state.material.lnx_particle_fade: + if mat_state.material.lnx_particle_flag and lnx.utils.get_rp().lnx_particles == 'GPU' and mat_state.material.lnx_particle_fade: frag.write('fragColor[0].rgb *= p_fade;') @@ -695,10 +702,10 @@ def make_forward_base(con_mesh, parse_opacity=False, transluc_pass=False): if '_Brdf' in wrd.world_defs: frag.write('envl.rgb *= 1.0 - F;') if '_Rad' in wrd.world_defs: - frag.write('envl += prefilteredColor * F;') + frag.write('envl += prefilteredColor * F * 1.5;') elif '_EnvCol' in wrd.world_defs: frag.add_uniform('vec3 backgroundCol', link='_backgroundCol') - frag.write('envl += backgroundCol * F;') + frag.write('envl += backgroundCol * F * 1.5;') frag.add_uniform('float envmapStrength', link='_envmapStrength') frag.write('envl *= envmapStrength * occlusion;') diff --git a/leenkx/blender/lnx/material/make_particle.py b/leenkx/blender/lnx/material/make_particle.py index 4e85a979..5ec7ef33 100644 --- a/leenkx/blender/lnx/material/make_particle.py +++ b/leenkx/blender/lnx/material/make_particle.py @@ -43,7 +43,7 @@ def write(vert, particle_info=None, shadowmap=False): for tex_slot in psettings.texture_slots: if not tex_slot: break if not tex_slot.use_map_size: break # TODO: check also for other influences - if tex_slot.texture and tex_slot.texture.use_color_ramp: + if tex_slot.texture.use_color_ramp: if tex_slot.texture.color_ramp and tex_slot.texture.color_ramp.elements: ramp_el_len = len(tex_slot.texture.color_ramp.elements.items()) for element in tex_slot.texture.color_ramp.elements: @@ -250,7 +250,7 @@ def write(vert, particle_info=None, shadowmap=False): vert.write('wnormal = normalize(rotate_around(wnormal, r_angle));') # Particle fade - if mat_state.material.lnx_particle_flag and lnx.utils.get_rp().lnx_particles == 'On' and mat_state.material.lnx_particle_fade: + if mat_state.material.lnx_particle_flag and lnx.utils.get_rp().lnx_particles == 'GPU' and mat_state.material.lnx_particle_fade: vert.add_out('float p_fade') vert.write('p_fade = sin(min((p_age / 2) * 3.141592, 3.141592));') diff --git a/leenkx/blender/lnx/material/make_voxel.py b/leenkx/blender/lnx/material/make_voxel.py index f589adbe..7851b954 100644 --- a/leenkx/blender/lnx/material/make_voxel.py +++ b/leenkx/blender/lnx/material/make_voxel.py @@ -93,7 +93,7 @@ def make_gi(context_id): # Voxelized particles particle = mat_state.material.lnx_particle_flag - if particle and rpdat.lnx_particles == 'On': + if particle and rpdat.lnx_particles == 'GPU': # make_particle.write(vert, particle_info=cycles.particle_info) frag.write_pre = True frag.write('const float p_index = 0;') diff --git a/leenkx/blender/lnx/material/mat_batch.py b/leenkx/blender/lnx/material/mat_batch.py index f21552d6..5cc4b836 100644 --- a/leenkx/blender/lnx/material/mat_batch.py +++ b/leenkx/blender/lnx/material/mat_batch.py @@ -59,6 +59,7 @@ def get_signature(mat, object: bpy.types.Object): sign += mat.lnx_billboard sign += '_skin' if lnx.utils.export_bone_data(object) else '0' sign += '_morph' if lnx.utils.export_morph_targets(object) else '0' + # sign += '_tilesheet' if mat.lnx_tilesheet_flag else '0' return sign def traverse_tree2(node, ar): diff --git a/leenkx/blender/lnx/material/shader.py b/leenkx/blender/lnx/material/shader.py index 95073588..83625945 100644 --- a/leenkx/blender/lnx/material/shader.py +++ b/leenkx/blender/lnx/material/shader.py @@ -1,3 +1,4 @@ +import bpy import lnx.utils if lnx.is_reload(__name__): @@ -286,7 +287,8 @@ class Shader: ar[0] = 'floats' ar[1] = ar[1].split('[', 1)[0] elif ar[0] == 'mat4' and '[' in ar[1]: - ar[0] = 'floats' + if not (ar[1].startswith('LWVPSpot') and not '_Clusters' in bpy.data.worlds['Lnx'].world_defs): #HACK: do not convert mat4 to floats when using single spot lights + ar[0] = 'floats' ar[1] = ar[1].split('[', 1)[0] self.context.add_constant(ar[0], ar[1], link=link, default_value=default_value, is_lnx_mat_param=is_lnx_mat_param) if top: diff --git a/leenkx/blender/lnx/props.py b/leenkx/blender/lnx/props.py index c2d746df..b4db41cd 100644 --- a/leenkx/blender/lnx/props.py +++ b/leenkx/blender/lnx/props.py @@ -31,7 +31,7 @@ else: lnx.enable_reload(__name__) # Leenkx version -lnx_version = '2025.1' +lnx_version = '2026.9' lnx_commit = '$Id: 6b2644d47db169cedd95593497cc283207d23a74 $' def get_project_html5_copy(self): @@ -151,7 +151,10 @@ def init_properties(): bpy.types.World.lnx_project_version_autoinc = BoolProperty(name="Auto-increment Build Number", description="Auto-increment build number", default=True, update=assets.invalidate_compiler_cache) bpy.types.World.lnx_project_bundle = StringProperty(name="Bundle", description="Exported project bundle", default="org.leenkx3d", update=assets.invalidate_compiler_cache, set=set_project_bundle, get=get_project_bundle) # External Blend Files - bpy.types.World.lnx_external_blends_path = StringProperty(name="External Blends", description="Directory containing external blend files to include in export", default="", subtype='DIR_PATH', update=assets.invalidate_compiler_cache) + if bpy.app.version >= (4, 5, 0): + bpy.types.World.lnx_external_blends_path = StringProperty(name="External Blends", description="Directory containing external blend files to include in export", default="", subtype='DIR_PATH', update=assets.invalidate_compiler_cache, options={'PATH_SUPPORTS_BLEND_RELATIVE'}) + else: + bpy.types.World.lnx_external_blends_path = StringProperty(name="External Blends", description="Directory containing external blend files to include in export", default="", subtype='DIR_PATH', update=assets.invalidate_compiler_cache) # Android Settings bpy.types.World.lnx_project_android_sdk_min = IntProperty(name="Minimal Version SDK", description="Minimal Version Android SDK", default=23, min=14, max=30, update=assets.invalidate_compiler_cache) bpy.types.World.lnx_project_android_sdk_target = IntProperty(name="Target Version SDK", description="Target Version Android SDK", default=26, min=26, max=30, update=assets.invalidate_compiler_cache) @@ -196,8 +199,14 @@ def init_properties(): bpy.types.World.lnx_project_win_build_cpu = IntProperty(name="CPU Count", description="Specifies the maximum number of concurrent processes to use when building", default=1, min=1, max=multiprocessing.cpu_count()) bpy.types.World.lnx_project_win_build_open = BoolProperty(name="Open Build Directory", description="Open the build directory after successfully assemble", default=False) - bpy.types.World.lnx_project_icon = StringProperty(name="Icon (PNG)", description="Exported project icon, must be a PNG image", default="", subtype="FILE_PATH", update=assets.invalidate_compiler_cache) - bpy.types.World.lnx_project_root = StringProperty(name="Root", description="Set root folder for linked assets", default="", subtype="DIR_PATH", update=assets.invalidate_compiler_cache) + if bpy.app.version >= (4, 5, 0): + bpy.types.World.lnx_project_icon = StringProperty(name="Icon (PNG)", description="Exported project icon, must be a PNG image", default="", subtype="FILE_PATH", update=assets.invalidate_compiler_cache, options={'PATH_SUPPORTS_BLEND_RELATIVE'}) + else: + bpy.types.World.lnx_project_icon = StringProperty(name="Icon (PNG)", description="Exported project icon, must be a PNG image", default="", subtype="FILE_PATH", update=assets.invalidate_compiler_cache) + if bpy.app.version >= (4, 5, 0): + bpy.types.World.lnx_project_root = StringProperty(name="Root", description="Set root folder for linked assets", default="", subtype="DIR_PATH", update=assets.invalidate_compiler_cache, options={'PATH_SUPPORTS_BLEND_RELATIVE'}) + else: + bpy.types.World.lnx_project_root = StringProperty(name="Root", description="Set root folder for linked assets", default="", subtype="DIR_PATH", update=assets.invalidate_compiler_cache) bpy.types.World.lnx_physics = EnumProperty( items=[('Disabled', 'Disabled', 'Disabled'), ('Auto', 'Auto', 'Auto'), @@ -418,9 +427,6 @@ def init_properties(): bpy.types.Object.lnx_relative_physics_constraint = BoolProperty(name="Relative Physics Constraint", description="Add physics constraint relative to the parent object or collection when spawned", default=False) bpy.types.Object.lnx_animation_enabled = BoolProperty(name="Animation", description="Enable skinning & timeline animation", default=True) - bpy.types.Object.lnx_tilesheet = StringProperty(name="Tilesheet", description="Set tilesheet animation", default='') - bpy.types.Object.lnx_tilesheet_action = StringProperty(name="Tilesheet Action", description="Set startup action", default='') - bpy.types.Object.lnx_use_custom_tilesheet_node = BoolProperty(name="Use custom tilesheet node", description="Use custom tilesheet shader node", default=False) # For speakers bpy.types.Speaker.lnx_play_on_start = BoolProperty(name="Play on Start", description="Play this sound automatically", default=False) bpy.types.Speaker.lnx_loop = BoolProperty(name="Loop", description="Loop this sound", default=False) @@ -616,8 +622,31 @@ def init_properties(): description="Particles have independent transform updates following emitter compared to a static baked particle system used if emitters dont generally move around.", default=True ) - bpy.types.ParticleSettings.lnx_is_unique = BoolProperty(name="Is Unique", description="Make this particle system look different each time it starts", default=False) + bpy.types.ParticleSettings.lnx_is_unique = BoolProperty(name="Is Unique", description="Make this particle system look different each time it starts. Only affects GPU particles. Default behavior for CPU particles", default=False) + bpy.types.ParticleSettings.lnx_local_coords = BoolProperty(name="Local Coords", description="Keep spawned particles parented to their emitter. Only affects CPU particles. Default behavior for GPU particles at the moment", default=False) bpy.types.ParticleSettings.lnx_loop = BoolProperty(name="Loop", description="Loop this particle system", default=False) + bpy.types.ParticleSettings.lnx_use_rotations = BoolProperty(name="Use Rotations", description="Enable particle rotations", default=False) + bpy.types.ParticleSettings.lnx_rotation_mode = EnumProperty( + name="Rotation Mode", + description="Rotation orientation mode", + items=[ + ('NONE', "None", "None"), + ('NOR', "Normal", "Normal"), + ('NOR_TAN', "Normal-Tangent", "Normal-Tangent"), + ('VEL', "Velocity/Hair", "Velocity/Hair"), + ('GLOB_X', "Global X", "Global X"), + ('GLOB_Y', "Global Y", "Global Y"), + ('GLOB_Z', "Global Z", "Global Z"), + ('OB_X', "Object X", "Object X"), + ('OB_Y', "Object Y", "Object Y"), + ('OB_Z', "Object Z", "Object Z"), + ], + default='NONE' + ) + bpy.types.ParticleSettings.lnx_rotation_factor_random = FloatProperty(name="Rotation Random", description="Random rotation factor", default=0.0, min=0.0, max=1.0) + bpy.types.ParticleSettings.lnx_phase_factor = FloatProperty(name="Phase", description="Rotation phase factor", default=0.0, min=0.0, max=1.0) + bpy.types.ParticleSettings.lnx_phase_factor_random = FloatProperty(name="Phase Random", description="Random rotation phase factor", default=0.0, min=0.0, max=1.0) + bpy.types.ParticleSettings.lnx_use_dynamic_rotation = BoolProperty(name="Dynamic Rotation", description="Enable dynamic rotation updates", default=False) bpy.types.ParticleSettings.lnx_count_mult = FloatProperty(name="Multiply Count", description="Multiply particle count when rendering in Leenkx", default=1.0) # Actions bpy.types.Action.lnx_root_motion_pos = BoolProperty(name="Root Motion Position", description="Enable position root motion", default=False) diff --git a/leenkx/blender/lnx/props_renderpath.py b/leenkx/blender/lnx/props_renderpath.py index 9fd0542d..90ea3574 100644 --- a/leenkx/blender/lnx/props_renderpath.py +++ b/leenkx/blender/lnx/props_renderpath.py @@ -66,7 +66,7 @@ def update_preset(self, context): rpdat.rp_stereo = False rpdat.rp_voxelgi_resolution = '32' rpdat.lnx_voxelgi_size = 0.125 - rpdat.rp_voxels = 'Voxel GI' + rpdat.rp_voxels = 'Off' rpdat.rp_render_to_texture = True rpdat.rp_supersampling = '1' rpdat.rp_antialiasing = 'SMAA' @@ -142,7 +142,7 @@ def update_preset(self, context): rpdat.rp_hdr = True rpdat.rp_background = 'World' rpdat.rp_stereo = False - rpdat.rp_voxels = 'Voxel GI' + rpdat.rp_voxels = 'Off' rpdat.rp_voxelgi_resolution = '64' rpdat.lnx_voxelgi_size = 0.125 rpdat.lnx_voxelgi_step = 0.01 @@ -444,7 +444,7 @@ class LnxRPListItem(bpy.types.PropertyGroup): rp_draw_order: EnumProperty( items=[('Auto', 'Auto', 'Auto'), ('Distance', 'Distance', 'Distance'), - ('Shader', 'Shader', 'Shader')], + ('Index', 'Index', 'Index')], name='Draw Order', description='Sort objects', default='Auto', update=assets.invalidate_compiled_data) rp_depth_texture: BoolProperty(name="Depth Texture", description="Current render-path state", default=False) rp_depth_texture_state: EnumProperty( @@ -669,9 +669,10 @@ class LnxRPListItem(bpy.types.PropertyGroup): ('Off', 'Off', 'Off')], name='Shape key', description='Enable shape keys', default='On', update=assets.invalidate_shader_cache) lnx_particles: EnumProperty( - items=[('On', 'On', 'On'), + items=[('GPU', 'GPU', 'GPU'), + ('CPU', 'CPU', 'CPU'), ('Off', 'Off', 'Off')], - name='Particles', description='Enable particle simulation', default='On', update=assets.invalidate_shader_cache) + name='Particles', description='Enable particle simulation', default='GPU', update=assets.invalidate_shader_cache) # Material override flags lnx_culling: BoolProperty(name="Culling", default=True) lnx_two_sided_area_light: BoolProperty(name="Two-Sided Area Light", description="Emit light from both faces of area plane", default=False, update=assets.invalidate_shader_cache) diff --git a/leenkx/blender/lnx/props_tilesheet.py b/leenkx/blender/lnx/props_tilesheet.py index 80498415..9b014ec4 100644 --- a/leenkx/blender/lnx/props_tilesheet.py +++ b/leenkx/blender/lnx/props_tilesheet.py @@ -1,211 +1,120 @@ import bpy from bpy.props import * + +class LnxTilesheetEventListItem(bpy.types.PropertyGroup): + """An event triggered on a specific frame within a tilesheet action.""" + name: StringProperty( + name="Event Name", + description="Name of the event to trigger", + default="event") + + frame_prop: IntProperty( + name="Frame", + description="Frame number when this event triggers", + default=0, + min=0) + + class LnxTilesheetActionListItem(bpy.types.PropertyGroup): + """An action (animation sequence) within a tilesheet with per-action properties.""" name: StringProperty( name="Name", - description="A name for this item", + description="Name of this tilesheet action", default="Untitled") start_prop: IntProperty( name="Start", - description="A name for this item", + description="Starting frame index", default=0) end_prop: IntProperty( name="End", - description="A name for this item", + description="Ending frame index", default=0) loop_prop: BoolProperty( name="Loop", - description="A name for this item", + description="Whether this action should loop", default=True) -class LNX_UL_TilesheetActionList(bpy.types.UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): - # We could write some code to decide which icon to use here... - custom_icon = 'OBJECT_DATAMODE' - - # Make sure your code supports all 3 layout types - if self.layout_type in {'DEFAULT', 'COMPACT'}: - layout.prop(item, "name", text="", emboss=False, icon=custom_icon) - - elif self.layout_type in {'GRID'}: - layout.alignment = 'CENTER' - layout.label(text="", icon = custom_icon) - -class LnxTilesheetActionListNewItem(bpy.types.Operator): - # Add a new item to the list - bl_idname = "lnx_tilesheetactionlist.new_item" - bl_label = "Add a new item" - - def execute(self, context): - wrd = bpy.data.worlds['Lnx'] - trait = wrd.lnx_tilesheetlist[wrd.lnx_tilesheetlist_index] - trait.lnx_tilesheetactionlist.add() - trait.lnx_tilesheetactionlist_index = len(trait.lnx_tilesheetactionlist) - 1 - return{'FINISHED'} - -class LnxTilesheetActionListDeleteItem(bpy.types.Operator): - """Delete the selected item from the list""" - bl_idname = "lnx_tilesheetactionlist.delete_item" - bl_label = "Deletes an item" - - @classmethod - def poll(self, context): - """Enable if there's something in the list""" - wrd = bpy.data.worlds['Lnx'] - if len(wrd.lnx_tilesheetlist) == 0: - return False - trait = wrd.lnx_tilesheetlist[wrd.lnx_tilesheetlist_index] - return len(trait.lnx_tilesheetactionlist) > 0 - - def execute(self, context): - wrd = bpy.data.worlds['Lnx'] - trait = wrd.lnx_tilesheetlist[wrd.lnx_tilesheetlist_index] - list = trait.lnx_tilesheetactionlist - index = trait.lnx_tilesheetactionlist_index - - list.remove(index) - - if index > 0: - index = index - 1 - - trait.lnx_tilesheetactionlist_index = index - return{'FINISHED'} - -class LnxTilesheetActionListMoveItem(bpy.types.Operator): - """Move an item in the list""" - bl_idname = "lnx_tilesheetactionlist.move_item" - bl_label = "Move an item in the list" - bl_options = {'INTERNAL'} - - direction: EnumProperty( - items=( - ('UP', 'Up', ""), - ('DOWN', 'Down', "") - )) - - @classmethod - def poll(self, context): - """Enable if there's something in the list""" - wrd = bpy.data.worlds['Lnx'] - if len(wrd.lnx_tilesheetlist) == 0: - return False - trait = wrd.lnx_tilesheetlist[wrd.lnx_tilesheetlist_index] - return len(trait.lnx_tilesheetactionlist) > 0 - - def move_index(self): - # Move index of an item render queue while clamping it - wrd = bpy.data.worlds['Lnx'] - trait = wrd.lnx_tilesheetlist[wrd.lnx_tilesheetlist_index] - index = trait.lnx_tilesheetactionlist_index - list_length = len(trait.lnx_tilesheetactionlist) - 1 - new_index = 0 - - if self.direction == 'UP': - new_index = index - 1 - elif self.direction == 'DOWN': - new_index = index + 1 - - new_index = max(0, min(new_index, list_length)) - trait.lnx_tilesheetactionlist.move(index, new_index) - trait.lnx_tilesheetactionlist_index = new_index - - def execute(self, context): - wrd = bpy.data.worlds['Lnx'] - trait = wrd.lnx_tilesheetlist[wrd.lnx_tilesheetlist_index] - list = trait.lnx_tilesheetactionlist - index = trait.lnx_tilesheetactionlist_index - - if self.direction == 'DOWN': - neighbor = index + 1 - self.move_index() - - elif self.direction == 'UP': - neighbor = index - 1 - self.move_index() - else: - return{'CANCELLED'} - return{'FINISHED'} - -class LnxTilesheetListItem(bpy.types.PropertyGroup): - name: StringProperty( - name="Name", - description="A name for this item", - default="Untitled") - tilesx_prop: IntProperty( name="Tiles X", - description="A name for this item", - default=0) + description="Number of horizontal tiles for this action", + default=1, + min=1) tilesy_prop: IntProperty( name="Tiles Y", - description="A name for this item", - default=0) + description="Number of vertical tiles for this action", + default=1, + min=1) - framerate_prop: FloatProperty( + framerate_prop: IntProperty( name="Frame Rate", - description="A name for this item", - default=4.0) + description="Animation frame rate for this action (frames per second)", + default=4, + min=1) - lnx_tilesheetactionlist: CollectionProperty(type=LnxTilesheetActionListItem) - lnx_tilesheetactionlist_index: IntProperty(name="Index for lnx_tilesheetactionlist", default=0) + mesh_prop: StringProperty( + name="Mesh", + description="Optional mesh data to swap to when playing this action (brings its own material/texture/UVs)", + default="") -class LNX_UL_TilesheetList(bpy.types.UIList): + # Events list for this action + events: CollectionProperty(type=LnxTilesheetEventListItem) + events_index: IntProperty(name="Event Index", default=0) + +class LNX_UL_TilesheetActionList(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): - # We could write some code to decide which icon to use here... - custom_icon = 'OBJECT_DATAMODE' + custom_icon = 'PLAY' - # Make sure your code supports all 3 layout types if self.layout_type in {'DEFAULT', 'COMPACT'}: layout.prop(item, "name", text="", emboss=False, icon=custom_icon) - elif self.layout_type in {'GRID'}: layout.alignment = 'CENTER' layout.label(text="", icon=custom_icon) -class LnxTilesheetListNewItem(bpy.types.Operator): - """Add a new item to the list""" - bl_idname = "lnx_tilesheetlist.new_item" - bl_label = "Add a new item" +class LnxTilesheetActionListNewItem(bpy.types.Operator): + """Add a new action to the tilesheet""" + bl_idname = "lnx_tilesheetactionlist.new_item" + bl_label = "Add Action" def execute(self, context): - wrd = bpy.data.worlds['Lnx'] - wrd.lnx_tilesheetlist.add() - wrd.lnx_tilesheetlist_index = len(wrd.lnx_tilesheetlist) - 1 - return{'FINISHED'} + obj = context.object + if obj is None: + return {'CANCELLED'} + obj.lnx_tilesheet_actionlist.add() + obj.lnx_tilesheet_actionlist_index = len(obj.lnx_tilesheet_actionlist) - 1 + return {'FINISHED'} -class LnxTilesheetListDeleteItem(bpy.types.Operator): - """Delete the selected item from the list""" - bl_idname = "lnx_tilesheetlist.delete_item" - bl_label = "Deletes an item" +class LnxTilesheetActionListDeleteItem(bpy.types.Operator): + """Delete the selected action from the tilesheet""" + bl_idname = "lnx_tilesheetactionlist.delete_item" + bl_label = "Delete Action" @classmethod - def poll(self, context): - """ Enable if there's something in the list """ - wrd = bpy.data.worlds['Lnx'] - return len(wrd.lnx_tilesheetlist) > 0 + def poll(cls, context): + obj = context.object + return obj is not None and len(obj.lnx_tilesheet_actionlist) > 0 def execute(self, context): - wrd = bpy.data.worlds['Lnx'] - list = wrd.lnx_tilesheetlist - index = wrd.lnx_tilesheetlist_index + obj = context.object + action_list = obj.lnx_tilesheet_actionlist + index = obj.lnx_tilesheet_actionlist_index - list.remove(index) + action_list.remove(index) if index > 0: index = index - 1 - wrd.lnx_tilesheetlist_index = index - return{'FINISHED'} + obj.lnx_tilesheet_actionlist_index = index + return {'FINISHED'} -class LnxTilesheetListMoveItem(bpy.types.Operator): - """Move an item in the list""" - bl_idname = "lnx_tilesheetlist.move_item" - bl_label = "Move an item in the list" +class LnxTilesheetActionListMoveItem(bpy.types.Operator): + """Move an action in the list""" + bl_idname = "lnx_tilesheetactionlist.move_item" + bl_label = "Move Action" bl_options = {'INTERNAL'} direction: EnumProperty( @@ -215,56 +124,266 @@ class LnxTilesheetListMoveItem(bpy.types.Operator): )) @classmethod - def poll(self, context): - """ Enable if there's something in the list. """ - wrd = bpy.data.worlds['Lnx'] - return len(wrd.lnx_tilesheetlist) > 0 - - def move_index(self): - # Move index of an item render queue while clamping it - wrd = bpy.data.worlds['Lnx'] - index = wrd.lnx_tilesheetlist_index - list_length = len(wrd.lnx_tilesheetlist) - 1 - new_index = 0 - - if self.direction == 'UP': - new_index = index - 1 - elif self.direction == 'DOWN': - new_index = index + 1 - - new_index = max(0, min(new_index, list_length)) - wrd.lnx_tilesheetlist.move(index, new_index) - wrd.lnx_tilesheetlist_index = new_index + def poll(cls, context): + obj = context.object + return obj is not None and len(obj.lnx_tilesheet_actionlist) > 0 def execute(self, context): - wrd = bpy.data.worlds['Lnx'] - list = wrd.lnx_tilesheetlist - index = wrd.lnx_tilesheetlist_index + obj = context.object + action_list = obj.lnx_tilesheet_actionlist + index = obj.lnx_tilesheet_actionlist_index + list_length = len(action_list) - 1 - if self.direction == 'DOWN': - neighbor = index + 1 - self.move_index() + if self.direction == 'UP': + new_index = max(0, index - 1) + else: # DOWN + new_index = min(list_length, index + 1) - elif self.direction == 'UP': - neighbor = index - 1 - self.move_index() + action_list.move(index, new_index) + obj.lnx_tilesheet_actionlist_index = new_index + return {'FINISHED'} + + +class LnxTilesheetEventListNewItem(bpy.types.Operator): + """Add a new event to the current action""" + bl_idname = "lnx_tilesheetactionlist.new_event" + bl_label = "Add Event" + + @classmethod + def poll(cls, context): + obj = context.object + return obj is not None and len(obj.lnx_tilesheet_actionlist) > 0 + + def execute(self, context): + obj = context.object + if obj.lnx_tilesheet_actionlist_index < 0: + return {'CANCELLED'} + action = obj.lnx_tilesheet_actionlist[obj.lnx_tilesheet_actionlist_index] + action.events.add() + action.events_index = len(action.events) - 1 + return {'FINISHED'} + + +class LnxTilesheetEventListDeleteItem(bpy.types.Operator): + """Delete the selected event from the current action""" + bl_idname = "lnx_tilesheetactionlist.delete_event" + bl_label = "Delete Event" + + @classmethod + def poll(cls, context): + obj = context.object + if obj is None or len(obj.lnx_tilesheet_actionlist) == 0: + return False + if obj.lnx_tilesheet_actionlist_index < 0: + return False + action = obj.lnx_tilesheet_actionlist[obj.lnx_tilesheet_actionlist_index] + return len(action.events) > 0 + + def execute(self, context): + obj = context.object + action = obj.lnx_tilesheet_actionlist[obj.lnx_tilesheet_actionlist_index] + events = action.events + index = action.events_index + + events.remove(index) + + if index > 0: + action.events_index = index - 1 + + return {'FINISHED'} + + +class LnxTilesheetSlice(bpy.types.Operator): + """Slice the UV map based on tile dimensions - scales UVs to fit one tile""" + bl_idname = "lnx_tilesheet.slice" + bl_label = "Slice" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + obj = context.object + if obj is None or obj.type != 'MESH': + return False + if len(obj.lnx_tilesheet_actionlist) == 0: + return False + if obj.lnx_tilesheet_actionlist_index < 0: + return False + action = obj.lnx_tilesheet_actionlist[obj.lnx_tilesheet_actionlist_index] + # Check if action has a mesh specified, and that mesh has UVs + if action.mesh_prop != '': + mesh_data = bpy.data.meshes.get(action.mesh_prop) + if mesh_data is None or not mesh_data.uv_layers: + return False else: - return{'CANCELLED'} - return{'FINISHED'} + # Fall back to object's own mesh + if not obj.data.uv_layers: + return False + return True + + def execute(self, context): + obj = context.object + + if obj.lnx_tilesheet_actionlist_index < 0: + self.report({'ERROR'}, "No action selected") + return {'CANCELLED'} + + action = obj.lnx_tilesheet_actionlist[obj.lnx_tilesheet_actionlist_index] + tiles_x = action.tilesx_prop + tiles_y = action.tilesy_prop + + if tiles_x < 1 or tiles_y < 1: + self.report({'ERROR'}, "Tiles X and Y must be at least 1") + return {'CANCELLED'} + + # Get mesh from action's mesh_prop, or fall back to object's mesh + if action.mesh_prop != '': + mesh = bpy.data.meshes.get(action.mesh_prop) + if mesh is None: + self.report({'ERROR'}, f"Mesh '{action.mesh_prop}' not found") + return {'CANCELLED'} + mesh_name = action.mesh_prop + else: + mesh = obj.data + mesh_name = obj.data.name + + if not mesh.uv_layers: + self.report({'ERROR'}, f"Mesh '{mesh_name}' has no UV layers") + return {'CANCELLED'} + + uv_layer = mesh.uv_layers.active.data + + # Calculate target tile size + tile_width = 1.0 / tiles_x + tile_height = 1.0 / tiles_y + + # Find current UV bounding box + min_u = min_v = float('inf') + max_u = max_v = float('-inf') + + for loop_uv in uv_layer: + min_u = min(min_u, loop_uv.uv[0]) + max_u = max(max_u, loop_uv.uv[0]) + min_v = min(min_v, loop_uv.uv[1]) + max_v = max(max_v, loop_uv.uv[1]) + + current_width = max_u - min_u + current_height = max_v - min_v + + if current_width == 0 or current_height == 0: + self.report({'ERROR'}, f"UV map on '{mesh_name}' has zero dimensions") + return {'CANCELLED'} + + # Scale and position UVs to fit in first tile (0,0) to (tile_width, tile_height) + for loop_uv in uv_layer: + # Normalize to 0-1 range + norm_u = (loop_uv.uv[0] - min_u) / current_width + norm_v = (loop_uv.uv[1] - min_v) / current_height + # Scale to tile size + loop_uv.uv[0] = norm_u * tile_width + loop_uv.uv[1] = norm_v * tile_height + + mesh.update() + + self.report({'INFO'}, f"UVs sliced to {tiles_x}x{tiles_y} grid (tile size: {tile_width:.3f} x {tile_height:.3f})") + return {'FINISHED'} + + +class LNX_PT_TilesheetPanel(bpy.types.Panel): + bl_label = "Leenkx Tilesheet" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "object" + bl_options = {'DEFAULT_CLOSED'} + + @classmethod + def poll(cls, context): + return context.object is not None and context.object.type == 'MESH' + + def draw_header(self, context): + obj = context.object + self.layout.prop(obj, "lnx_tilesheet_enabled", text="") + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + obj = context.object + + layout.enabled = obj.lnx_tilesheet_enabled + + # Start action dropdown + layout.prop_search(obj, "lnx_tilesheet_default_action", obj, "lnx_tilesheet_actionlist", text="Start Action") + + row = layout.row() + row.prop(obj, "lnx_tilesheet_flipx") + row.prop(obj, "lnx_tilesheet_flipy") + + # Actions list + layout.separator() + layout.label(text="Actions") + rows = 2 + if len(obj.lnx_tilesheet_actionlist) > 1: + rows = 4 + row = layout.row() + row.template_list("LNX_UL_TilesheetActionList", "The_List", obj, "lnx_tilesheet_actionlist", obj, "lnx_tilesheet_actionlist_index", rows=rows) + col = row.column(align=True) + col.operator("lnx_tilesheetactionlist.new_item", icon='ADD', text="") + col.operator("lnx_tilesheetactionlist.delete_item", icon='REMOVE', text="") + + if len(obj.lnx_tilesheet_actionlist) > 1: + col.separator() + op = col.operator("lnx_tilesheetactionlist.move_item", icon='TRIA_UP', text="") + op.direction = 'UP' + op = col.operator("lnx_tilesheetactionlist.move_item", icon='TRIA_DOWN', text="") + op.direction = 'DOWN' + + # Selected action details (per-action properties) + if obj.lnx_tilesheet_actionlist_index >= 0 and len(obj.lnx_tilesheet_actionlist) > 0: + adat = obj.lnx_tilesheet_actionlist[obj.lnx_tilesheet_actionlist_index] + box = layout.box() + # Grid dimensions + row = box.row() + row.use_property_split = False + row.prop(adat, "tilesx_prop") + row.prop(adat, "tilesy_prop") + row = box.row() + row.operator("lnx_tilesheet.slice", text="Slice") + # Frame range + row = box.row() + row.use_property_split = False + row.prop(adat, "start_prop") + row.prop(adat, "end_prop") + # Framerate and loop + box.prop(adat, "framerate_prop") + box.prop(adat, "loop_prop") + # Optional mesh (dropdown from bpy.data.meshes) + box.prop_search(adat, "mesh_prop", bpy.data, "meshes", text="Mesh") + + # Events section + box.separator() + box.label(text="Events") + row = box.row() + col = row.column() + for i, evt in enumerate(adat.events): + evt_row = col.row(align=True) + evt_row.prop(evt, "name", text="") + evt_row.prop(evt, "frame_prop", text="Frame") + row = box.row(align=True) + row.operator("lnx_tilesheetactionlist.new_event", icon='ADD', text="Add Event") + row.operator("lnx_tilesheetactionlist.delete_event", icon='REMOVE', text="Remove Event") __REG_CLASSES = ( + LnxTilesheetEventListItem, LnxTilesheetActionListItem, LNX_UL_TilesheetActionList, LnxTilesheetActionListNewItem, LnxTilesheetActionListDeleteItem, LnxTilesheetActionListMoveItem, - - LnxTilesheetListItem, - LNX_UL_TilesheetList, - LnxTilesheetListNewItem, - LnxTilesheetListDeleteItem, - LnxTilesheetListMoveItem, + LnxTilesheetEventListNewItem, + LnxTilesheetEventListDeleteItem, + LnxTilesheetSlice, + LNX_PT_TilesheetPanel, ) __reg_classes, unregister = bpy.utils.register_classes_factory(__REG_CLASSES) @@ -272,5 +391,28 @@ __reg_classes, unregister = bpy.utils.register_classes_factory(__REG_CLASSES) def register(): __reg_classes() - bpy.types.World.lnx_tilesheetlist = CollectionProperty(type=LnxTilesheetListItem) - bpy.types.World.lnx_tilesheetlist_index = IntProperty(name="Index for lnx_tilesheetlist", default=0) + # Tilesheet properties on Object (one tilesheet per object) + bpy.types.Object.lnx_tilesheet_enabled = BoolProperty( + name="Tilesheet", + description="Enable tilesheet animation for this object", + default=False) + + bpy.types.Object.lnx_tilesheet_default_action = StringProperty( + name="Start Action", + description="Start action to play on spawn", + default="") + + bpy.types.Object.lnx_tilesheet_flipx = BoolProperty( + name="Flip X", + description="Flip the tilesheet horizontally", + default=False) + + bpy.types.Object.lnx_tilesheet_flipy = BoolProperty( + name="Flip Y", + description="Flip the tilesheet vertically", + default=False) + + bpy.types.Object.lnx_tilesheet_actionlist = CollectionProperty(type=LnxTilesheetActionListItem) + bpy.types.Object.lnx_tilesheet_actionlist_index = IntProperty( + name="Action Index", + default=0) diff --git a/leenkx/blender/lnx/props_traits.py b/leenkx/blender/lnx/props_traits.py index b7bf79ce..6a7afd44 100644 --- a/leenkx/blender/lnx/props_traits.py +++ b/leenkx/blender/lnx/props_traits.py @@ -29,7 +29,7 @@ else: ICON_HAXE = ui_icons.get_id('haxe') ICON_NODES = 'NODETREE' -ICON_CANVAS = 'NODE_COMPOSITING' +ICON_CANVAS = 'WINDOW' ICON_BUNDLED = ui_icons.get_id('bundle') ICON_WASM = ui_icons.get_id('wasm') @@ -131,7 +131,7 @@ class LNX_UL_TraitList(bpy.types.UIList): elif item.type_prop == "WebAssembly": custom_icon_value = ICON_WASM elif item.type_prop == "UI Canvas": - custom_icon = "NODE_COMPOSITING" + custom_icon = "WINDOW" elif item.type_prop == "Bundled Script": custom_icon_value = ICON_BUNDLED elif item.type_prop == "Logic Nodes": @@ -996,7 +996,7 @@ def draw_traits_panel(layout: bpy.types.UILayout, obj: Union[bpy.types.Object, b row.operator("lnx.new_canvas", icon="FILE_NEW").is_object = is_object column = row.column(align=True) column.enabled = item.canvas_name_prop != '' - column.operator("lnx.edit_canvas", icon="NODE_COMPOSITING").is_object = is_object + column.operator("lnx.edit_canvas", icon="WINDOW").is_object = is_object refresh_op = "lnx.refresh_object_scripts" if is_object else "lnx.refresh_scripts" row.operator(refresh_op, text="Refresh", icon="FILE_REFRESH") diff --git a/leenkx/blender/lnx/props_traits_props.py b/leenkx/blender/lnx/props_traits_props.py index 61c40c3d..37ad22dc 100644 --- a/leenkx/blender/lnx/props_traits_props.py +++ b/leenkx/blender/lnx/props_traits_props.py @@ -16,8 +16,7 @@ PROP_TYPE_ICONS = { "CameraObject": "CAMERA_DATA", "LightObject": "LIGHT_DATA", "MeshObject": "MESH_DATA", - "SpeakerObject": "OUTLINER_DATA_SPEAKER", - "TSceneFormat": "SCENE_DATA" + "SpeakerObject": "OUTLINER_DATA_SPEAKER" } @@ -61,8 +60,7 @@ class LnxTraitPropListItem(bpy.types.PropertyGroup): ("CameraObject", "Camera Object", "Camera Object Type"), ("LightObject", "Light Object", "Light Object Type"), ("MeshObject", "Mesh Object", "Mesh Object Type"), - ("SpeakerObject", "Speaker Object", "Speaker Object Type"), - ("TSceneFormat", "Scene", "Scene Type")), + ("SpeakerObject", "Speaker Object", "Speaker Object Type")), name="Type", description="The type of this property", default="String") @@ -74,7 +72,6 @@ class LnxTraitPropListItem(bpy.types.PropertyGroup): value_vec3: FloatVectorProperty(name="Value", size=3) value_vec4: FloatVectorProperty(name="Value", size=4) value_object: PointerProperty(name="Value", type=bpy.types.Object, poll=filter_objects) - value_scene: PointerProperty(name="Value", type=bpy.types.Scene) else: type: EnumProperty( items=( @@ -90,8 +87,7 @@ class LnxTraitPropListItem(bpy.types.PropertyGroup): ("CameraObject", "Camera Object", "Camera Object Type"), ("LightObject", "Light Object", "Light Object Type"), ("MeshObject", "Mesh Object", "Mesh Object Type"), - ("SpeakerObject", "Speaker Object", "Speaker Object Type"), - ("TSceneFormat", "Scene", "Scene Type")), + ("SpeakerObject", "Speaker Object", "Speaker Object Type")), name="Type", description="The type of this property", default="String", @@ -104,7 +100,6 @@ class LnxTraitPropListItem(bpy.types.PropertyGroup): value_vec3: FloatVectorProperty(name="Value", size=3, override={"LIBRARY_OVERRIDABLE"}) value_vec4: FloatVectorProperty(name="Value", size=4, override={"LIBRARY_OVERRIDABLE"}) value_object: PointerProperty(name="Value", type=bpy.types.Object, poll=filter_objects, override={"LIBRARY_OVERRIDABLE"}) - value_scene: PointerProperty(name="Value", type=bpy.types.Scene, override={"LIBRARY_OVERRIDABLE"}) def set_value(self, val): # Would require way too much effort, so it's out of scope here. @@ -153,10 +148,6 @@ class LnxTraitPropListItem(bpy.types.PropertyGroup): if self.value_object is not None: return self.value_object.name return "" - if self.type == "TSceneFormat": - if self.value_scene is not None: - return self.value_scene.name - return "" return self.value_string @@ -176,8 +167,6 @@ class LNX_UL_PropList(bpy.types.UIList): if self.layout_type in {'DEFAULT', 'COMPACT'}: if item.type.endswith("Object"): sp.prop_search(item, "value_object", context.scene, "objects", text="", icon=custom_icon) - elif item.type.endswith("TSceneFormat"): - sp.prop_search(item, "value_scene", bpy.data, "scenes", text="", icon=custom_icon) else: use_emboss = item.type in ("Bool", "String") sp.prop(item, item_value_ref, text="", emboss=use_emboss) diff --git a/leenkx/blender/lnx/props_ui.py b/leenkx/blender/lnx/props_ui.py index cb0ebaa3..b36be293 100644 --- a/leenkx/blender/lnx/props_ui.py +++ b/leenkx/blender/lnx/props_ui.py @@ -92,16 +92,6 @@ class LNX_PT_ObjectPropsPanel(bpy.types.Panel): if obj.type == 'MESH': layout.prop(obj, 'lnx_instanced') - wrd = bpy.data.worlds['Lnx'] - layout.prop_search(obj, "lnx_tilesheet", wrd, "lnx_tilesheetlist", text="Tilesheet") - if obj.lnx_tilesheet != '': - selected_ts = None - for ts in wrd.lnx_tilesheetlist: - if ts.name == obj.lnx_tilesheet: - selected_ts = ts - break - layout.prop_search(obj, "lnx_tilesheet_action", selected_ts, "lnx_tilesheetactionlist", text="Action") - layout.prop(obj, "lnx_use_custom_tilesheet_node") # Properties list lnx.props_properties.draw_properties(layout, obj) @@ -227,6 +217,7 @@ class LNX_PT_ParticlesPropsPanel(bpy.types.Panel): layout.prop(obj.settings, 'lnx_auto_start') layout.prop(obj.settings, 'lnx_dynamic_emitter') layout.prop(obj.settings, 'lnx_is_unique') + layout.prop(obj.settings, 'lnx_local_coords') layout.prop(obj.settings, 'lnx_loop') layout.prop(obj.settings, 'lnx_count_mult') @@ -864,11 +855,13 @@ class LNX_PT_LeenkxPlayerPanel(bpy.types.Panel): row.operator("lnx.play", icon="PLAY") else: if bpy.app.version < (3, 0, 0): - row.operator("lnx.stop", icon="CANCEL", text="") - elif bpy.app.version > (3, 0, 0) and bpy.app.version < (4, 3, 2): - row.operator("lnx.stop", icon="SEQUENCE_COLOR_01", text="") + row.operator("lnx.stop", icon="CANCEL") + elif bpy.app.version > (3, 0, 0) and bpy.app.version < (4, 5, 0): + row.operator("lnx.stop", icon="SEQUENCE_COLOR_01") + elif bpy.app.version >= (4, 5, 0): + row.operator("lnx.stop", icon="STRIP_COLOR_01") else: - row.operator("lnx.stop", icon="EVENT_MEDIASTOP", text="") + row.operator("lnx.stop", icon="EVENT_MEDIASTOP") row.operator("lnx.clean_menu", icon="BRUSH_DATA") col = layout.box().column() @@ -1369,6 +1362,7 @@ class LeenkxStopButton(bpy.types.Operator): elif state.proc_build != None: state.proc_build.terminate() state.proc_build = None + make.clear_external_scenes() lnx.write_probes.check_last_cmft_time() @@ -1550,11 +1544,13 @@ class LNX_PT_TopbarPanel(bpy.types.Panel): row.operator("lnx.play", icon="PLAY", text="") else: if bpy.app.version < (3, 0, 0): - row.operator("lnx.stop", icon="CANCEL", text="") - elif bpy.app.version > (3, 0, 0) and bpy.app.version < (4, 3, 2): - row.operator("lnx.stop", icon="SEQUENCE_COLOR_01", text="") + row.operator("lnx.stop", icon="CANCEL") + elif bpy.app.version > (3, 0, 0) and bpy.app.version < (4, 5, 0): + row.operator("lnx.stop", icon="SEQUENCE_COLOR_01") + elif bpy.app.version >= (4, 5, 0): + row.operator("lnx.stop", icon="STRIP_COLOR_01") else: - row.operator("lnx.stop", icon="EVENT_MEDIASTOP", text="") + row.operator("lnx.stop", icon="EVENT_MEDIASTOP") row.operator("lnx.clean_menu", icon="BRUSH_DATA", text="") row.operator("lnx.open_editor", icon="DESKTOP", text="") row.operator("lnx.open_project_folder", icon="FILE_FOLDER", text="") @@ -2435,64 +2431,6 @@ class LNX_PT_TerrainPanel(bpy.types.Panel): layout.operator('lnx.generate_terrain') layout.prop(scn, 'lnx_terrain_object') -class LNX_PT_TilesheetPanel(bpy.types.Panel): - bl_label = "Leenkx Tilesheet" - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "material" - bl_options = {'DEFAULT_CLOSED'} - - def draw(self, context): - layout = self.layout - layout.use_property_split = True - layout.use_property_decorate = False - wrd = bpy.data.worlds['Lnx'] - - rows = 2 - if len(wrd.lnx_tilesheetlist) > 1: - rows = 4 - row = layout.row() - row.template_list("LNX_UL_TilesheetList", "The_List", wrd, "lnx_tilesheetlist", wrd, "lnx_tilesheetlist_index", rows=rows) - col = row.column(align=True) - col.operator("lnx_tilesheetlist.new_item", icon='ADD', text="") - col.operator("lnx_tilesheetlist.delete_item", icon='REMOVE', text="") - - if len(wrd.lnx_tilesheetlist) > 1: - col.separator() - op = col.operator("lnx_tilesheetlist.move_item", icon='TRIA_UP', text="") - op.direction = 'UP' - op = col.operator("lnx_tilesheetlist.move_item", icon='TRIA_DOWN', text="") - op.direction = 'DOWN' - - if wrd.lnx_tilesheetlist_index >= 0 and len(wrd.lnx_tilesheetlist) > 0: - dat = wrd.lnx_tilesheetlist[wrd.lnx_tilesheetlist_index] - layout.prop(dat, "tilesx_prop") - layout.prop(dat, "tilesy_prop") - layout.prop(dat, "framerate_prop") - - layout.label(text='Actions') - rows = 2 - if len(dat.lnx_tilesheetactionlist) > 1: - rows = 4 - row = layout.row() - row.template_list("LNX_UL_TilesheetList", "The_List", dat, "lnx_tilesheetactionlist", dat, "lnx_tilesheetactionlist_index", rows=rows) - col = row.column(align=True) - col.operator("lnx_tilesheetactionlist.new_item", icon='ADD', text="") - col.operator("lnx_tilesheetactionlist.delete_item", icon='REMOVE', text="") - - if len(dat.lnx_tilesheetactionlist) > 1: - col.separator() - op = col.operator("lnx_tilesheetactionlist.move_item", icon='TRIA_UP', text="") - op.direction = 'UP' - op = col.operator("lnx_tilesheetactionlist.move_item", icon='TRIA_DOWN', text="") - op.direction = 'DOWN' - - if dat.lnx_tilesheetactionlist_index >= 0 and len(dat.lnx_tilesheetactionlist) > 0: - adat = dat.lnx_tilesheetactionlist[dat.lnx_tilesheetactionlist_index] - layout.prop(adat, "start_prop") - layout.prop(adat, "end_prop") - layout.prop(adat, "loop_prop") - class LnxPrintTraitsButton(bpy.types.Operator): bl_idname = 'lnx.print_traits' bl_label = 'Print All Traits' @@ -3058,7 +2996,6 @@ __REG_CLASSES = ( LNX_PT_LodPanel, LnxGenTerrainButton, LNX_PT_TerrainPanel, - LNX_PT_TilesheetPanel, LnxPrintTraitsButton, LNX_PT_MaterialNodePanel, LNX_OT_UpdateFileSDK, diff --git a/leenkx/blender/lnx/utils.py b/leenkx/blender/lnx/utils.py index 47ceeaf8..11d85070 100644 --- a/leenkx/blender/lnx/utils.py +++ b/leenkx/blender/lnx/utils.py @@ -380,7 +380,7 @@ def get_haxe_path(): if get_os() == 'win': return get_kha_path() + '/Tools/windows_x64/haxe.exe' elif get_os() == 'mac': - return get_kha_path() + '/Tools/macos/haxe' + return get_kha_path() + '/Tools/macos_x64/haxe' else: return get_kha_path() + '/Tools/linux_x64/haxe' @@ -484,7 +484,7 @@ def fetch_script_props(filename: str): # Property type is annotated if p_type is not None: - if p_type.startswith("iron.object.") or p_type == "iron.data.SceneFormat.TSceneFormat": + if p_type.startswith("iron.object."): p_type = p_type[12:] elif p_type.startswith("iron.math."): p_type = p_type[10:] @@ -562,7 +562,7 @@ def get_type_default_value(prop_type: str): if prop_type == "Float": return 0.0 if prop_type == "String" or prop_type in ( - "Object", "CameraObject", "LightObject", "MeshObject", "SpeakerObject", "TSceneFormat"): + "Object", "CameraObject", "LightObject", "MeshObject", "SpeakerObject"): return "" if prop_type == "Bool": return False @@ -804,13 +804,13 @@ def get_haxe_json_string(d: dict) -> str: return s def asset_name(bdata): - if bdata == None: - return None - s = bdata.name - # Append library name if linked - if bdata.library is not None: - s += '_' + bdata.library.name - return s + """Get the qualified asset name, with library suffix for linked data. + + For local assets, returns just the name. + For linked assets, returns 'name_libraryname' to ensure uniqueness. + """ + import lnx.linked_utils as linked_utils + return linked_utils.asset_name(bdata) def asset_path(s): """Remove leading '//'""" @@ -868,7 +868,7 @@ def check_blender_version(op: bpy.types.Operator): """Check whether the Blender version is supported by Leenkx, if not, report in UI. """ - 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)]: op.report({'INFO'}, 'INFO: For Leenkx to work correctly, use a Blender LTS version') diff --git a/leenkx/blender/lnx/write_data.py b/leenkx/blender/lnx/write_data.py index fd721ece..8ef01d9f 100644 --- a/leenkx/blender/lnx/write_data.py +++ b/leenkx/blender/lnx/write_data.py @@ -1,4 +1,5 @@ import glob +import importlib.util import io import json import os @@ -10,6 +11,7 @@ from typing import List import bpy import lnx.assets as assets +import lnx.log as log import lnx.make_renderpath as make_renderpath import lnx.make_state as state import lnx.utils @@ -17,6 +19,7 @@ import lnx.utils if lnx.is_reload(__name__): import lnx assets = lnx.reload_module(assets) + log = lnx.reload_module(log) make_renderpath = lnx.reload_module(make_renderpath) state = lnx.reload_module(state) lnx.utils = lnx.reload_module(lnx.utils) @@ -24,6 +27,96 @@ else: lnx.enable_reload(__name__) +def get_library_hooks(): + """Discover and load `lnx_hooks.py` from Libraries and Subprojects. + + Each hook module can define a write_main() function that returns a dict with: + - 'imports': Haxe import statements to add at the top + - 'main_pre': Code to add at the start of main() before Starter.main() + - 'main_post': Code to add at the end of main() after Starter.main() + - 'wrap_init': Tuple of (before, after) strings to wrap around the main() body + - 'priority': Integer for wrap_init nesting order (lower = outer, default = 0) + - 'defines': List of Haxe defines to add to khafile.js + - 'parameters': List of Haxe parameters to add to khafile.js + - 'assets': List of asset entries for khafile.js. Each entry can be: + - A string path: "Assets/icons.png" + - A tuple (path, options_dict): ("Assets/icons.png", {"noCompress": True}) + + Multiple wrap_init hooks are nested based on priority: + - Priority 0 (outer): LibA.init(function() { + - Priority 10 (inner): LibB.init(function() { + - Starter.main(); - Priority 10 (inner): }); + - Priority 0 (outer): }); + + Returns a dict with combined strings for each injection point. + """ + hooks = { + 'imports': '', # Haxe import statements + 'main_pre': '', # Code to add at the start of main() before Starter.main() + 'main_post': '', # Code to add at the end of main() after Starter.main() + 'wrap_inits': [], # List of (priority, before, after) tuples + 'defines': [], # List of Haxe defines for khafile.js + 'parameters': [], # List of Haxe parameters for khafile.js + 'assets': [] # List of assets for khafile.js + } + + project_path = lnx.utils.get_fp() + hook_dirs = [] + + # Check Libraries folder + libraries_path = os.path.join(project_path, 'Libraries') + if os.path.exists(libraries_path): + for lib in os.listdir(libraries_path): + lib_path = os.path.join(libraries_path, lib) + if os.path.isdir(lib_path): + hook_dirs.append((lib, lib_path)) + + # Check Subprojects folder + subprojects_path = os.path.join(project_path, 'Subprojects') + if os.path.exists(subprojects_path): + for lib in os.listdir(subprojects_path): + lib_path = os.path.join(subprojects_path, lib) + if os.path.isdir(lib_path): + hook_dirs.append((lib, lib_path)) + + # Load each hook module + for lib_name, lib_path in hook_dirs: + hook_file = os.path.join(lib_path, 'lnx_hooks.py') + if os.path.isfile(hook_file): + try: + spec = importlib.util.spec_from_file_location(f"lnx_hooks_{lib_name}", hook_file) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + if hasattr(module, 'write_main'): + result = module.write_main() + if result: + if 'imports' in result and result['imports']: + hooks['imports'] += result['imports'] + '\n' + if 'main_pre' in result and result['main_pre']: + hooks['main_pre'] += result['main_pre'] + '\n' + if 'main_post' in result and result['main_post']: + hooks['main_post'] += result['main_post'] + '\n' + if 'wrap_init' in result and result['wrap_init']: + priority = result.get('priority', 0) + hooks['wrap_inits'].append((priority, result['wrap_init'][0], result['wrap_init'][1])) + if 'defines' in result and result['defines']: + hooks['defines'].extend(result['defines']) + if 'parameters' in result and result['parameters']: + hooks['parameters'].extend(result['parameters']) + if 'assets' in result and result['assets']: + hooks['assets'].extend(result['assets']) + + log.info(f"Loaded library hook: {lib_name}") + except Exception as e: + log.warn(f"Failed to load hook from {lib_name}: {e}") + + # Sort wrap_inits by priority (lower = outer wrapper) + hooks['wrap_inits'].sort(key=lambda x: x[0]) + + return hooks + + def on_same_drive(path1: str, path2: str) -> bool: drive_path1, _ = os.path.splitdrive(path1) drive_path2, _ = os.path.splitdrive(path2) @@ -70,6 +163,9 @@ def write_khafilejs(is_play, export_physics: bool, export_navigation: bool, expo wrd = bpy.data.worlds['Lnx'] rpdat = lnx.utils.get_rp() + # Get library hooks for defines and parameters + hooks = get_library_hooks() + sdk_path = lnx.utils.get_sdk_path() rel_path = lnx.utils.get_relative_paths() # Convert absolute paths to relative project_path = lnx.utils.get_fp() @@ -78,13 +174,47 @@ def write_khafilejs(is_play, export_physics: bool, export_navigation: bool, expo # Whether to use relative paths for paths inside the SDK do_relpath_sdk = rel_path and on_same_drive(sdk_path, project_path) + # Determine if assets should go to data folder (used for hook assets and later) + use_data_dir = is_publish and (state.target == 'krom-windows' or state.target == 'krom-linux' or state.target == 'windows-hl' or state.target == 'linux-hl' or state.target == 'html5') + with open('khafile.js', 'w', encoding="utf-8") as khafile: khafile.write( """// Auto-generated let project = new Project('""" + lnx.utils.safesrc(wrd.lnx_project_name + '-' + wrd.lnx_project_version) + """'); -project.addSources('Sources'); """) + # Add library hook assets + for asset in hooks['assets']: + dest_opt = ', destination: "data/{name}"' if use_data_dir else '' + if isinstance(asset, tuple): + path, options = asset + opts = [] + for key, value in options.items(): + if isinstance(value, bool): + opts.append(f"{key}: {'true' if value else 'false'}") + elif isinstance(value, str): + opts.append(f"{key}: '{value}'") + else: + opts.append(f"{key}: {value}") + if use_data_dir and 'destination' not in options: + opts.append('destination: "data/{name}"') + opts_str = ', '.join(opts) + khafile.write(f'project.addAssets("{path}", {{ {opts_str} }});\n') + else: + khafile.write(f'project.addAssets("{asset}", {{ notinlist: true{dest_opt} }});\n') + + # Add library hook defines + for d in hooks['defines']: + khafile.write("project.addDefine('" + d + "');\n") + + for p in assets.khafile_params: + khafile.write("project.addParameter('" + p + "');\n") + + # Add library hook parameters + for p in hooks['parameters']: + khafile.write("project.addParameter('" + p + "');\n") + + khafile.write("project.addSources('Sources');\n") # Auto-add assets located in Bundled directory if os.path.exists('Bundled'): @@ -247,8 +377,7 @@ project.addSources('Sources'); shaders_path = os.path.relpath(shaders_path, project_path).replace('\\', '/') khafile.write('project.addShaders("' + shaders_path + '", { noprocessing: true, noembed: ' + str(noembed).lower() + ' });\n') - # Move assets for published game to /data folder - use_data_dir = is_publish and (state.target == 'krom-windows' or state.target == 'krom-linux' or state.target == 'windows-hl' or state.target == 'linux-hl' or state.target == 'html5') + # Add define for data directory usage if use_data_dir: assets.add_khafile_def('lnx_data_dir') @@ -353,6 +482,10 @@ project.addSources('Sources'); if rpdat.lnx_particles != 'Off': assets.add_khafile_def('lnx_particles') + if rpdat.lnx_particles == 'GPU': + assets.add_khafile_def('lnx_gpu_particles') + elif rpdat.lnx_particles == 'CPU': + assets.add_khafile_def('lnx_cpu_particles') if rpdat.rp_draw_order == 'Index': assets.add_khafile_def('lnx_draworder_index') @@ -369,9 +502,6 @@ project.addSources('Sources'); if wrd.lnx_winresize or state.target == 'html5': assets.add_khafile_def('lnx_resizable') - if get_winmode(wrd.lnx_winmode) == 1 and state.target.startswith('html5'): - assets.add_khafile_def('kha_html5_disable_automatic_size_adjust') - # if bpy.data.scenes[0].unit_settings.system_rotation == 'DEGREES': # assets.add_khafile_def('lnx_degrees') @@ -381,9 +511,6 @@ project.addSources('Sources'); for d in assets.khafile_defs: khafile.write("project.addDefine('" + d + "');\n") - for p in assets.khafile_params: - khafile.write("project.addParameter('" + p + "');\n") - if state.target.startswith('android'): bundle = 'org.leenkx3d.' + wrd.lnx_project_package if wrd.lnx_project_bundle == '' else wrd.lnx_project_bundle khafile.write("project.targetOptions.android_native.package = '{0}';\n".format(lnx.utils.safestr(bundle))) @@ -424,7 +551,10 @@ project.addSources('Sources'); khafile.write("project.targetOptions.ios.bundle = '{0}';\n".format(lnx.utils.safestr(bundle))) if wrd.lnx_project_icon != '': - shutil.copy(bpy.path.abspath(wrd.lnx_project_icon), project_path + '/icon.png') + icon_src = os.path.normpath(bpy.path.abspath(wrd.lnx_project_icon)) + icon_dst = os.path.normpath(os.path.join(project_path, 'icon.png')) + if not os.path.isfile(icon_dst) or not os.path.samefile(icon_src, icon_dst): + shutil.copy2(icon_src, icon_dst) if wrd.lnx_khafile is not None: khafile.write(wrd.lnx_khafile.as_string()) @@ -494,10 +624,18 @@ def write_mainhx(scene_name, resx, resy, is_play, is_publish): elif rpdat.rp_driver != 'Leenkx': pathpack = rpdat.rp_driver.lower() + # Get library hooks + hooks = get_library_hooks() + with open('Sources/Main.hx', 'w', encoding="utf-8") as f: f.write( """// Auto-generated -package;\n""") +package; +""") + + # Write hook imports + if hooks['imports']: + f.write(hooks['imports']) f.write(""" class Main { @@ -511,10 +649,9 @@ class Main { public static inline var voxelgiVoxelSize = """ + str(round(rpdat.lnx_voxelgi_size * 100) / 100) + """;""") if rpdat.rp_bloom: - if bpy.app.version <= (4, 2, 4): - f.write(f"public static var bloomRadius = {bpy.context.scene.eevee.bloom_radius if rpdat.lnx_bloom_follow_blender else rpdat.lnx_bloom_radius};") - else: - f.write(f"public static var bloomRadius = {rpdat.lnx_bloom_radius};") + follow_blender = rpdat.lnx_bloom_follow_blender if bpy.app.version < (4, 3, 0) else False + + f.write(f"public static var bloomRadius = {bpy.context.scene.eevee.bloom_radius if follow_blender else rpdat.lnx_bloom_radius};") if rpdat.lnx_rp_resolution == 'Custom': f.write(""" @@ -523,49 +660,71 @@ class Main { f.write("""\n public static function main() {""") + # Calculate base indentation (2 tabs = 8 spaces) + base_indent = " " + wrap_count = len(hooks['wrap_inits']) + + # Write wrap_init opening statements (outer to inner, sorted by priority) + for i, (priority, before, after) in enumerate(hooks['wrap_inits']): + indent = base_indent + (" " * i) + f.write("\n" + indent + before) + + # Calculate content indentation based on wrap depth + content_indent = base_indent + (" " * wrap_count) + + # Write main_pre hooks + if hooks['main_pre']: + f.write("\n" + content_indent + hooks['main_pre'].replace('\n', '\n' + content_indent)) + if rpdat.lnx_skin != 'Off': - f.write(""" - iron.object.BoneAnimation.skinMaxBones = """ + str(rpdat.lnx_skin_max_bones) + """;""") + f.write("\n" + content_indent + "iron.object.BoneAnimation.skinMaxBones = " + str(rpdat.lnx_skin_max_bones) + ";") if rpdat.rp_shadows: if rpdat.rp_shadowmap_cascades != '1': - f.write(""" - iron.object.LightObject.cascadeCount = """ + str(rpdat.rp_shadowmap_cascades) + """; - iron.object.LightObject.cascadeSplitFactor = """ + str(rpdat.lnx_shadowmap_split) + """;""") + f.write("\n" + content_indent + "iron.object.LightObject.cascadeCount = " + str(rpdat.rp_shadowmap_cascades) + ";") + f.write("\n" + content_indent + "iron.object.LightObject.cascadeSplitFactor = " + str(rpdat.lnx_shadowmap_split) + ";") if rpdat.lnx_shadowmap_bounds != 1.0: - f.write(""" - iron.object.LightObject.cascadeBounds = """ + str(rpdat.lnx_shadowmap_bounds) + """;""") + f.write("\n" + content_indent + "iron.object.LightObject.cascadeBounds = " + str(rpdat.lnx_shadowmap_bounds) + ";") if is_publish and wrd.lnx_loadscreen: asset_references = list(set(assets.assets)) loadscreen_class = 'leenkx.trait.internal.LoadingScreen' if os.path.isfile(lnx.utils.get_fp() + '/Sources/' + wrd.lnx_project_package + '/LoadingScreen.hx'): loadscreen_class = wrd.lnx_project_package + '.LoadingScreen' - f.write(""" - leenkx.system.Starter.numAssets = """ + str(len(asset_references)) + """; - leenkx.system.Starter.drawLoading = """ + loadscreen_class + """.render;""") + f.write("\n" + content_indent + "leenkx.system.Starter.numAssets = " + str(len(asset_references)) + ";") + f.write("\n" + content_indent + "leenkx.system.Starter.drawLoading = " + loadscreen_class + ".render;") if wrd.lnx_ui == 'Enabled': if wrd.lnx_canvas_img_scaling_quality == 'low': - f.write(""" - leenkx.ui.Canvas.imageScaleQuality = kha.graphics2.ImageScaleQuality.Low;""") + f.write("\n" + content_indent + "leenkx.ui.Canvas.imageScaleQuality = kha.graphics2.ImageScaleQuality.Low;") elif wrd.lnx_canvas_img_scaling_quality == 'high': - f.write(""" - leenkx.ui.Canvas.imageScaleQuality = kha.graphics2.ImageScaleQuality.High;""") - + f.write("\n" + content_indent + "leenkx.ui.Canvas.imageScaleQuality = kha.graphics2.ImageScaleQuality.High;") + + # Write Starter.main call + starter_indent = content_indent + " " + f.write("\n" + content_indent + "leenkx.system.Starter.main(") + f.write("\n" + starter_indent + "'" + lnx.utils.safestr(scene_name) + scene_ext + "',") + f.write("\n" + starter_indent + str(winmode) + ",") + f.write("\n" + starter_indent + ('true' if wrd.lnx_winresize else 'false') + ",") + f.write("\n" + starter_indent + ('true' if wrd.lnx_winminimize else 'false') + ",") + f.write("\n" + starter_indent + ('true' if (wrd.lnx_winresize and wrd.lnx_winmaximize) else 'false') + ",") + f.write("\n" + starter_indent + str(resx) + ",") + f.write("\n" + starter_indent + str(resy) + ",") + f.write("\n" + starter_indent + str(int(rpdat.lnx_samples_per_pixel)) + ",") + f.write("\n" + starter_indent + ('true' if wrd.lnx_vsync else 'false') + ",") + f.write("\n" + starter_indent + pathpack + ".renderpath.RenderPathCreator.get") + f.write("\n" + content_indent + ");") + + # Write main_post hooks + if hooks['main_post']: + f.write("\n" + content_indent + hooks['main_post'].replace('\n', '\n' + content_indent)) + + # Write wrap_init closing statements (inner to outer, reverse order) + for i, (priority, before, after) in enumerate(reversed(hooks['wrap_inits'])): + indent = base_indent + (" " * (wrap_count - 1 - i)) + f.write("\n" + indent + after) + f.write(""" - leenkx.system.Starter.main( - '""" + lnx.utils.safestr(scene_name) + scene_ext + """', - """ + str(winmode) + """, - """ + ('true' if wrd.lnx_winresize else 'false') + """, - """ + ('true' if wrd.lnx_winminimize else 'false') + """, - """ + ('true' if (wrd.lnx_winresize and wrd.lnx_winmaximize) else 'false') + """, - """ + str(resx) + """, - """ + str(resy) + """, - """ + str(int(rpdat.lnx_samples_per_pixel)) + """, - """ + ('true' if wrd.lnx_vsync else 'false') + """, - """ + pathpack + """.renderpath.RenderPathCreator.get - ); } }""") @@ -584,28 +743,27 @@ def write_indexhtml(w, h, is_publish): """ - """) + """) if rpdat.rp_stereo or wrd.lnx_winmode == 'Fullscreen': f.write(""" - -""") + """) f.write(""" """+html.escape( wrd.lnx_project_name)+""" - + """) if rpdat.rp_stereo or wrd.lnx_winmode == 'Fullscreen': f.write(""" - + """) else: if wrd.lnx_winmode != 'Headless': f.write(""" -

+

""") else: f.write(""" - + """) f.write(""" - + """) @@ -707,16 +865,13 @@ const float ssgiRadius = """ + str(round(rpdat.lnx_ssgi_radius * 100) / 100) + " """) if rpdat.rp_bloom: - follow_blender = rpdat.lnx_bloom_follow_blender + + follow_blender = rpdat.lnx_bloom_follow_blender if bpy.app.version < (4, 3, 0) else False + eevee_settings = bpy.context.scene.eevee - if bpy.app.version <= (4, 2, 4): - threshold = eevee_settings.bloom_threshold if follow_blender else rpdat.lnx_bloom_threshold - strength = eevee_settings.bloom_intensity if follow_blender else rpdat.lnx_bloom_strength - knee = eevee_settings.bloom_knee if follow_blender else rpdat.lnx_bloom_knee - else: - threshold = rpdat.lnx_bloom_threshold - strength = rpdat.lnx_bloom_strength - knee = rpdat.lnx_bloom_knee + threshold = eevee_settings.bloom_threshold if follow_blender else rpdat.lnx_bloom_threshold + strength = eevee_settings.bloom_intensity if follow_blender else rpdat.lnx_bloom_strength + knee = eevee_settings.bloom_knee if follow_blender else rpdat.lnx_bloom_knee f.write( """const float bloomThreshold = """ + str(round(threshold * 100) / 100) + """; diff --git a/leenkx/blender/lnx/write_probes.py b/leenkx/blender/lnx/write_probes.py index a57636b2..38cab572 100644 --- a/leenkx/blender/lnx/write_probes.py +++ b/leenkx/blender/lnx/write_probes.py @@ -160,7 +160,7 @@ def write_probes(image_filepath: str, disable_hdr: bool, from_srgb: bool, cached kraffiti_path = kha_path + '/Kinc/Tools/windows_x64/kraffiti.exe' elif lnx.utils.get_os() == 'mac': cmft_path = '"' + sdk_path + '/lib/leenkx_tools/cmft/cmft-osx"' - kraffiti_path = '"' + kha_path + '/Kinc/Tools/macos/kraffiti"' + kraffiti_path = '"' + kha_path + '/Kinc/Tools/macos_x64/kraffiti"' else: cmft_path = '"' + sdk_path + '/lib/leenkx_tools/cmft/cmft-linux64"' kraffiti_path = '"' + kha_path + '/Kinc/Tools/linux_x64/kraffiti"' @@ -439,8 +439,10 @@ def write_sky_irradiance(base_name): def write_color_irradiance(base_name, col): """Constant color irradiance""" - # Adjust to Cycles - irradiance_floats = [col[0] * 1.13, col[1] * 1.13, col[2] * 1.13] + # Match shIrradiance()'s c4 factor so a constant color environment + # decodes back to the same diffuse radiance at strength 1. + sh_l00 = 1.0 / 0.886227 + irradiance_floats = [col[0] * sh_l00, col[1] * sh_l00, col[2] * sh_l00] for i in range(0, 24): irradiance_floats.append(0.0) diff --git a/lib/haxebullet/Sources/webidl/Module.hx b/lib/haxebullet/Sources/webidl/Module.hx index 3e0d10d1..172d5bcb 100644 --- a/lib/haxebullet/Sources/webidl/Module.hx +++ b/lib/haxebullet/Sources/webidl/Module.hx @@ -214,7 +214,7 @@ class Module { if( v.ret.t != TVoid ) e = { expr : EReturn(e), pos : p }; else if( isConstr ) - e = macro this = $e; + e = macro this = cast $e; return e; } diff --git a/lib/haxerecast/Sources/webidl/Module.hx b/lib/haxerecast/Sources/webidl/Module.hx index e97c3765..5bff48e3 100644 --- a/lib/haxerecast/Sources/webidl/Module.hx +++ b/lib/haxerecast/Sources/webidl/Module.hx @@ -214,7 +214,7 @@ class Module { if( v.ret.t != TVoid ) e = { expr : EReturn(e), pos : p }; else if( isConstr ) - e = macro this = $e; + e = macro this = cast $e; return e; }