diff --git a/leenkx/Shaders/probe_cubemap/probe_cubemap.json b/leenkx/Shaders/probe_cubemap/probe_cubemap.json index d2bd1600..7dcad164 100644 --- a/leenkx/Shaders/probe_cubemap/probe_cubemap.json +++ b/leenkx/Shaders/probe_cubemap/probe_cubemap.json @@ -3,8 +3,8 @@ { "name": "probe_cubemap", "depth_write": false, - "compare_mode": "less", - "cull_mode": "clockwise", + "compare_mode": "greater", + "cull_mode": "counter_clockwise", "blend_source": "blend_one", "blend_destination": "blend_one", "blend_operation": "add", diff --git a/leenkx/Shaders/probe_planar/probe_planar.json b/leenkx/Shaders/probe_planar/probe_planar.json index ba39b1b4..455281bc 100644 --- a/leenkx/Shaders/probe_planar/probe_planar.json +++ b/leenkx/Shaders/probe_planar/probe_planar.json @@ -3,8 +3,8 @@ { "name": "probe_planar", "depth_write": false, - "compare_mode": "less", - "cull_mode": "clockwise", + "compare_mode": "greater", + "cull_mode": "counter_clockwise", "blend_source": "blend_one", "blend_destination": "blend_one", "blend_operation": "add", diff --git a/leenkx/Sources/iron/RenderPath.hx b/leenkx/Sources/iron/RenderPath.hx index f90f5101..92b4a330 100644 --- a/leenkx/Sources/iron/RenderPath.hx +++ b/leenkx/Sources/iron/RenderPath.hx @@ -34,7 +34,6 @@ import iron.math.Quat; class RenderPath { public static var active: RenderPath; #if lnx_vr - static var vrSimulateMode: Bool = false; static var vrCameraOffsetSet:Bool = false; static var vrCameraOffset:Vec4 = new Vec4(); static var wasVRPresenting:Bool = false; @@ -45,6 +44,9 @@ class RenderPath { public static var vrCenterCameraWorld:Mat4 = null; static var vrOriginalSuperSample:Float = -1.0; + #end + + static var vrSimulateMode: Bool = false; public static inline function isVRPresenting(): Bool { #if (kha_webgl && lnx_vr) return kha.vr.VrInterface.instance != null && kha.vr.VrInterface.instance.IsPresenting(); @@ -57,8 +59,6 @@ class RenderPath { return vrSimulateMode; } - #end - public var frameScissor = false; public var frameScissorX = 0; public var frameScissorY = 0; diff --git a/leenkx/Sources/iron/Scene.hx b/leenkx/Sources/iron/Scene.hx index 8a88a4e7..76efc5ae 100644 --- a/leenkx/Sources/iron/Scene.hx +++ b/leenkx/Sources/iron/Scene.hx @@ -1003,7 +1003,7 @@ class Scene { } if (o.vertex_groups != null) { - cast(object, MeshObject).vertexGroups = new Map(); + object.vertexGroups = new Map(); for (p in o.vertex_groups) { var verts:Array = []; @@ -1014,7 +1014,7 @@ class Scene { verts.push(new iron.math.Vec4(data[i * 3], data[i * 3 + 1], data[i * 3 + 2], 1.0)); } } - cast(object, MeshObject).vertexGroups.set(p.name, verts); + object.vertexGroups.set(p.name, verts); } } diff --git a/leenkx/Sources/iron/data/Geometry.hx b/leenkx/Sources/iron/data/Geometry.hx index 6d07d38e..b1b5495a 100644 --- a/leenkx/Sources/iron/data/Geometry.hx +++ b/leenkx/Sources/iron/data/Geometry.hx @@ -58,6 +58,7 @@ class Geometry { // Skinned #if lnx_skin public var skeletonTransformsI: Array = null; + public var skeletonBoneRefs: Array = null; public var actions: Map> = new Map(); public var mats: Map> = new Map(); #end @@ -326,6 +327,33 @@ class Geometry { // Skinned #if lnx_skin + public function addArmature(armature: iron.data.Armature) { + for (a in armature.actions) { + addAction(a.bones, a.name); + } + } + + public function addAction(bones: Array, name: String) { + if (bones == null || skeletonBoneRefs == null) return; + if (actions.get(name) != null) return; + var actionBones: Array = []; + + for (s in skeletonBoneRefs) { + for (b in bones) { + if (b.name == s) { + actionBones.push(b); + } + } + } + actions.set(name, actionBones); + + var actionMats: Array = []; + for (b in actionBones) { + actionMats.push(Mat4.fromFloat32Array(b.transform.values)); + } + mats.set(name, actionMats); + } + public function initSkeletonTransforms(transformsI: Array) { skeletonTransformsI = []; for (t in transformsI) { diff --git a/leenkx/Sources/iron/data/MeshData.hx b/leenkx/Sources/iron/data/MeshData.hx index cfdb3e71..6be5d998 100644 --- a/leenkx/Sources/iron/data/MeshData.hx +++ b/leenkx/Sources/iron/data/MeshData.hx @@ -108,6 +108,7 @@ class MeshData { // Skinned #if lnx_skin if (raw.skin != null) { + dat.geom.skeletonBoneRefs = raw.skin.bone_ref_array; dat.geom.initSkeletonTransforms(raw.skin.transformsI); } #end diff --git a/leenkx/Sources/iron/format/bmp/Reader.hx b/leenkx/Sources/iron/format/bmp/Reader.hx index d46895c7..6331f20c 100644 --- a/leenkx/Sources/iron/format/bmp/Reader.hx +++ b/leenkx/Sources/iron/format/bmp/Reader.hx @@ -49,7 +49,7 @@ class Reader { * @see https://msdn.microsoft.com/en-us/library/windows/desktop/dd318229(v=vs.85).aspx * @see https://en.wikipedia.org/wiki/BMP_file_format#Bitmap_file_header */ - public function read() : format.bmp.Data { + public function read() : iron.format.bmp.Data { // Read Header for (b in ["B".code, "M".code]) { if (input.readByte() != b) throw "Invalid header"; diff --git a/leenkx/Sources/iron/format/gif/Writer.hx b/leenkx/Sources/iron/format/gif/Writer.hx index a563bc71..880fde80 100644 --- a/leenkx/Sources/iron/format/gif/Writer.hx +++ b/leenkx/Sources/iron/format/gif/Writer.hx @@ -1,5 +1,5 @@ -package format.gif; -import format.gif.Data; +package iron.format.gif; +import iron.format.gif.Data; import haxe.ds.Vector; import haxe.io.Bytes; import haxe.io.Output; diff --git a/leenkx/Sources/iron/format/jpg/WriterOriginal.hx b/leenkx/Sources/iron/format/jpg/WriterOriginal.hx index 4d4fd63f..61eb8c98 100644 --- a/leenkx/Sources/iron/format/jpg/WriterOriginal.hx +++ b/leenkx/Sources/iron/format/jpg/WriterOriginal.hx @@ -29,7 +29,7 @@ */ package iron.format.jpg; -class Writer { +class WriterOriginal { var ZigZag: Array; // Static table initialization diff --git a/leenkx/Sources/iron/object/Animation.hx b/leenkx/Sources/iron/object/Animation.hx index a5056d2d..d67d40a4 100644 --- a/leenkx/Sources/iron/object/Animation.hx +++ b/leenkx/Sources/iron/object/Animation.hx @@ -40,11 +40,6 @@ class Animation { public var paused = false; var frameTime: FastFloat = 1 / 60; - var blendTime: FastFloat = 0.0; - var blendCurrent: FastFloat = 0.0; - var blendFactor: FastFloat = 0.0; - - var lastFrameIndex = -1; var markerEvents: MapVoid>>> = null; public var activeActions: Map = null; @@ -63,8 +58,6 @@ class Animation { public function play(action = "", onComplete: Void->Void = null, blendTime = 0.0, speed = 1.0, loop = true) { if (blendTime > 0) { - this.blendTime = blendTime; - this.blendCurrent = 0.0; frameIndex = 0; time = 0.0; } @@ -93,6 +86,7 @@ class Animation { } public function update(delta: FastFloat) { + if (paused) return; if(activeActions == null) return; for(sampler in activeActions){ @@ -160,24 +154,25 @@ class Animation { while (checkFrameIndex(track.frames, time, frameIndex, speed)) frameIndex += sign; // Marker events - if (markerEvents != null && anim.marker_names != null && frameIndex != lastFrameIndex) { - if(markerEvents.get(sampler) != null){ + if (markerEvents != null && anim.marker_names != null && frameIndex != sampler.offsetOld) { + var markerListeners = markerEvents.get(sampler); + if (markerListeners != null) { + var fromFrame = sampler.offsetOld < 0 ? + (speed > 0 ? -1 : track.frames[track.frames.length - 1] + 1) : + track.frames[sampler.offsetOld]; + var toFrame = track.frames[frameIndex]; for (i in 0...anim.marker_frames.length) { - if (frameIndex == anim.marker_frames[i]) { - var markerAct = markerEvents.get(sampler); - var ar = markerAct.get(anim.marker_names[i]); - if (ar != null) for (f in ar) f(); - } else { - for (j in 0...(frameIndex - lastFrameIndex)) { - if (lastFrameIndex + j + 1 == anim.marker_frames[i]) { - var markerAct = markerEvents.get(sampler); - var ar = markerAct.get(anim.marker_names[i]); - if (ar != null) for (f in ar) f(); - } - } + var markerFrame = anim.marker_frames[i]; + var crossed = + (speed > 0 && toFrame >= fromFrame && markerFrame > fromFrame && markerFrame <= toFrame) || + (speed < 0 && toFrame <= fromFrame && markerFrame <= fromFrame && markerFrame > toFrame) || + (speed > 0 && toFrame < fromFrame && (markerFrame > fromFrame || markerFrame <= toFrame)) || + (speed < 0 && toFrame > fromFrame && (markerFrame >= toFrame || markerFrame < fromFrame)); + if (crossed) { + var listeners = markerListeners.get(anim.marker_names[i]); + if (listeners != null) for (listener in listeners) listener(); } } - lastFrameIndex = frameIndex; } } @@ -191,8 +186,8 @@ class Animation { else { frameIndex -= sign; sampler.paused = true; + if (sampler.onComplete != null) for(func in sampler.onComplete){ func();}; } - if (sampler.onComplete != null) for(func in sampler.onComplete){ func();}; sampler.trackEnd = true; } diff --git a/leenkx/Sources/iron/object/BoneAnimation.hx b/leenkx/Sources/iron/object/BoneAnimation.hx index 1f5a2bbf..62ce8f13 100644 --- a/leenkx/Sources/iron/object/BoneAnimation.hx +++ b/leenkx/Sources/iron/object/BoneAnimation.hx @@ -112,6 +112,10 @@ class BoneAnimation extends Animation { this.data = mo != null ? mo.data : null; this.isSkinned = data != null ? data.isSkinned : false; if (this.isSkinned) { + if (armature != null && data.geom.skeletonBoneRefs != null + && data.geom.actions.keys().hasNext() == false) { + data.geom.addArmature(armature); + } var boneSize = 12; // Dual-quat skinning + scaling this.skinBuffer = new Float32Array(skinMaxBones * boneSize); for (i in 0...this.skinBuffer.length) this.skinBuffer[i] = 0; @@ -205,18 +209,37 @@ class BoneAnimation extends Animation { } function setAction(action: String) { - if (armature == null) return; - armature.initMats(); - var a = armature.getAction(action); - if (a == null) return; - skeletonBones = a.bones; - skeletonMats = a.mats; - if(! rootMotionCacheInit) skeletonMats.push(Mat4.identity()); + if (isSkinned) { + skeletonBones = data.geom.actions.get(action); + var geomMats = data.geom.mats.get(action); + if (geomMats != null) { + skeletonMats = []; + for (i in 0...geomMats.length) { + var m = Mat4.identity(); + m.setFrom(geomMats[i]); + skeletonMats.push(m); + } + } + else skeletonMats = null; + } + if (skeletonBones == null) { + if (armature == null) return; + armature.initMats(); + var a = armature.getAction(action); + if (a == null) return; + skeletonBones = a.bones; + skeletonMats = a.mats; + } + if (skeletonMats.length == skeletonBones.length) skeletonMats.push(Mat4.identity()); // Root motion cache rootMotionCacheInit = true; setMats(); } function getAction(action: String): Array { + if (isSkinned) { + var bones = data.geom.actions.get(action); + if (bones != null) return bones; + } if (armature == null) return null; armature.initMats(); var a = armature.getAction(action); @@ -251,15 +274,91 @@ class BoneAnimation extends Animation { updateAnimation = f; } + var blendSampler1: ActionSampler = null; + var blendSampler2: ActionSampler = null; + var blendMats: Array = null; + + public function blend(action1: String, action2: String, factor: FastFloat) { + var actionName1: String = getName(action1); + var actionName2: String = getName(action2); + if (blendSampler1 == null || blendSampler1.action != actionName1) { + blendSampler1 = new ActionSampler(actionName1); + registerAction("blendSampler1", blendSampler1); + } + if (blendSampler2 == null || blendSampler2.action != actionName2) { + blendSampler2 = new ActionSampler(actionName2); + registerAction("blendSampler2", blendSampler2); + } + blendSampler1.paused = false; + blendSampler2.paused = false; + if (transitionSampler != null) { + transitionSampler.paused = true; + transitionSampler = null; + } + if (currentPlaySampler != null) currentPlaySampler.paused = true; + currentPlaySampler = blendSampler1; + if (skeletonMats == null) setAction(actionName1); + if (skeletonMats == null) return; + if (blendMats == null) blendMats = initMatsEmpty(); + updateAnimation = function(mats) { + sampleAction(blendSampler1, mats); + sampleAction(blendSampler2, blendMats); + blendAction(mats, blendMats, mats, factor); + } + } + + var playSamplers: Map = []; + var currentPlaySampler: ActionSampler = null; + var transitionSampler: ActionSampler = null; + var transitionMats: Array = null; + var transitionTime: FastFloat = 0.0; + var transitionDuration: FastFloat = 0.0; + override public function play(action = "", onComplete: Void->Void = null, blendTime = 0.2, speed = 1.0, loop = true) { var actionName: String = getName(action); if (actionName != "") { setAction(actionName); super.play(actionName, onComplete, blendTime, speed, loop); - var tempAnimParam = new ActionSampler(actionName); - registerAction("tempAction", tempAnimParam); + var sampler = playSamplers.get(actionName); + if (sampler == null) { + if (activeActions != null) sampler = activeActions.get(actionName); + if (sampler == null) sampler = new ActionSampler(actionName); + playSamplers.set(actionName, sampler); + } + if (transitionSampler != null) { + transitionSampler.paused = true; + transitionSampler = null; + } + if (blendTime > 0 && currentPlaySampler != null && currentPlaySampler != sampler && skeletonMats != null) { + transitionSampler = currentPlaySampler; + if (transitionMats == null) transitionMats = initMatsEmpty(); + transitionTime = 0.0; + transitionDuration = blendTime; + if (blendSampler1 != null && blendSampler1 != transitionSampler) blendSampler1.paused = true; + if (blendSampler2 != null && blendSampler2 != transitionSampler) blendSampler2.paused = true; + } + else if (currentPlaySampler != null) currentPlaySampler.paused = true; + currentPlaySampler = sampler; + sampler.speed = speed; + sampler.loop = loop; + sampler.onComplete = onComplete != null ? [onComplete] : null; + sampler.restartAction(); + registerAction(actionName, sampler); updateAnimation = function(mats){ - sampleAction(tempAnimParam, mats); + if (transitionSampler != null) { + sampleAction(transitionSampler, transitionMats); + sampleAction(sampler, mats); + var f = transitionTime / transitionDuration; + if (f > 1.0) f = 1.0; + f = f * f * (3.0 - 2.0 * f); // Smoothstep + blendAction(transitionMats, mats, mats, f, -1, 0.0); + transitionTime += delta; + if (transitionTime >= transitionDuration) { + transitionSampler.paused = true; + transitionSampler = null; + } + } + else sampleAction(sampler, mats); } } } @@ -279,6 +378,7 @@ class BoneAnimation extends Animation { #end super.update(delta); + if (paused) return; if(updateAnimation != null) { updateAnimation(skeletonMats); } @@ -540,12 +640,14 @@ class BoneAnimation extends Animation { inline function interpolateSample(track: TTrack, m: Mat4, t: FastFloat, ti: Int, sign: Int) { + var tj = ti + sign; + if (tj < 0 || tj >= track.frames.length) tj = ti; var t1 = track.frames[ti] * frameTime; - var t2 = track.frames[ti + sign] * frameTime; - var s: FastFloat = (t - t1) / (t2 - t1); // Linear + var t2 = track.frames[tj] * frameTime; + var s: FastFloat = t2 != t1 ? (t - t1) / (t2 - t1) : 0.0; // Linear m1.setF32(track.values, ti * 16); // Offset to 4x4 matrix array - m2.setF32(track.values, (ti + sign) * 16); + m2.setF32(track.values, tj * 16); // Decompose m1.decompose(vpos, q1, vscl); @@ -643,7 +745,7 @@ class BoneAnimation extends Animation { return actionMats != null ? actionMats[getBoneIndex(bone)] : null; } - function getWorldMat(actionMats: Array, bone: TObj): Mat4 { + public function getWorldMat(actionMats: Array, bone: TObj): Mat4 { if (applyParent == null) { applyParent = []; for (m in actionMats) applyParent.push(true); @@ -728,7 +830,7 @@ class BoneAnimation extends Animation { // Skip if root motion is disabled if(j < 0) continue; // Use Action 2 - if(skeletonBones[j].bone_layers[layerMask] || layerMask < 0){ + if(layerMask < 0 || skeletonBones[j].bone_layers[layerMask]){ resultMat[i].setFrom(actionMats2[i]); } // Use Action 1 if not in layer @@ -744,7 +846,7 @@ class BoneAnimation extends Animation { // Skip if root motion is disabled if(j < 0) continue; // Blend - if(skeletonBones[j].bone_layers[j] || layerMask < 0) { + if(layerMask < 0 || skeletonBones[j].bone_layers[layerMask]) { // Decompose m.setFrom(actionMats1[i]); m1.setFrom(actionMats2[i]); @@ -786,8 +888,10 @@ class BoneAnimation extends Animation { } else{ for(i in 0...baseActionMats.length){ + var j = i == skeletonBones.length ? rootMotionIndex : i; + if(j < 0) continue; - if(skeletonBones[i].bone_layers[layerMask] || layerMask < 0) { + if(layerMask < 0 || skeletonBones[j].bone_layers[layerMask]) { // Decompose m.setFrom(baseActionMats[i]); m1.setFrom(addActionMats[i]); diff --git a/leenkx/Sources/iron/object/Constraint.hx b/leenkx/Sources/iron/object/Constraint.hx index 93763664..d9feeb28 100644 --- a/leenkx/Sources/iron/object/Constraint.hx +++ b/leenkx/Sources/iron/object/Constraint.hx @@ -13,7 +13,10 @@ class Constraint { } public function apply(transform: Transform) { - if (target == null && raw.target != null) target = Scene.active.getChild(raw.target).transform; + if (target == null && raw.target != null) { + var o = Scene.active.getChild(raw.target); + if (o != null) target = o.transform; + } if (target == null && raw.type != "LIMIT_LOCATION" && raw.type != "LIMIT_ROTATION" && raw.type != "LIMIT_SCALE") return; diff --git a/leenkx/Sources/iron/object/MeshObject.hx b/leenkx/Sources/iron/object/MeshObject.hx index 86c79611..eb329cd9 100644 --- a/leenkx/Sources/iron/object/MeshObject.hx +++ b/leenkx/Sources/iron/object/MeshObject.hx @@ -48,7 +48,6 @@ class MeshObject extends Object { #if lnx_morph_target public var morphTarget: MorphTarget = null; #end - public var vertexGroups: Map> = null; #if lnx_veloc public var prevMatrix = Mat4.identity(); @@ -240,6 +239,7 @@ class MeshObject extends Object { return setCulled(isShadow, true); } + #if rp_shadowmap if (isShadow && light.data.raw.type == "sun") { var frame = RenderPath.active.frame; if (shadowCameraCullFrame == frame && shadowCameraCullCamera == camera && @@ -270,10 +270,8 @@ class MeshObject extends Object { } } } + #end } - - //culled = false; - //return culled; return setCulled(isShadow, false); } diff --git a/leenkx/Sources/iron/object/Object.hx b/leenkx/Sources/iron/object/Object.hx index 345aeec5..938eafb3 100644 --- a/leenkx/Sources/iron/object/Object.hx +++ b/leenkx/Sources/iron/object/Object.hx @@ -15,6 +15,7 @@ class Object { public var transform: Transform; public var constraints: Array = null; public var traits: Array = []; + public var vertexGroups: Map> = null; public var parent: Object = null; public var children: Array = []; diff --git a/leenkx/Sources/iron/object/ObjectAnimation.hx b/leenkx/Sources/iron/object/ObjectAnimation.hx index 198daaff..9c08067d 100644 --- a/leenkx/Sources/iron/object/ObjectAnimation.hx +++ b/leenkx/Sources/iron/object/ObjectAnimation.hx @@ -17,6 +17,7 @@ class ObjectAnimation extends Animation { public var oactions: Array; var oaction: TObj; var s0: FastFloat = 0.0; + var delta: FastFloat = 0; var bezierFrameIndex = -1; var updateAnimation: Map -> Void; @@ -26,7 +27,11 @@ class ObjectAnimation extends Animation { public var transformMap: Map; var defaultSampler: ActionSampler = null; - static inline var DEFAULT_SAMPLER_ID = "__object_default_action__"; + var playSamplers: Map = []; + var transitionSampler: ActionSampler = null; + var transitionMap: Map = null; + var transitionTime: FastFloat = 0.0; + var transitionDuration: FastFloat = 0.0; public static var trackNames: Array = [ "xloc", "yloc", "zloc", "xrot", "yrot", "zrot", @@ -67,27 +72,59 @@ class ObjectAnimation extends Animation { oaction = getAction(this.action); if (oaction != null) { isSampled = oaction.sampled != null && oaction.sampled; - if (defaultSampler != null) { - deRegisterAction(DEFAULT_SAMPLER_ID); + var sampler = playSamplers.get(this.action); + if (sampler == null) { + if (activeActions != null) sampler = activeActions.get(this.action); + if (sampler == null) sampler = new ActionSampler(this.action); + playSamplers.set(this.action, sampler); } - var callbacks = onComplete != null ? [onComplete] : null; - defaultSampler = new ActionSampler(this.action, speed, loop, false, callbacks); - registerAction(DEFAULT_SAMPLER_ID, defaultSampler); - if (paused) defaultSampler.paused = true; + if (transitionSampler != null) { + transitionSampler.paused = true; + transitionSampler = null; + } + if (blendTime > 0 && defaultSampler != null && defaultSampler != sampler) { + transitionSampler = defaultSampler; + transitionMap = initTransformMap(); + transitionTime = 0.0; + transitionDuration = blendTime; + } + else if (defaultSampler != null) defaultSampler.paused = true; + defaultSampler = sampler; + sampler.speed = speed; + sampler.loop = loop; + sampler.onComplete = onComplete != null ? [onComplete] : null; + sampler.restartAction(); + registerAction(this.action, sampler); updateAnimation = function(map: Map) { - sampleAction(defaultSampler, map); + if (transitionSampler != null) { + sampleAction(transitionSampler, transitionMap); + sampleAction(sampler, map); + var f = transitionTime / transitionDuration; + if (f > 1.0) f = 1.0; + f = f * f * (3.0 - 2.0 * f); // Smoothstep + blendActionObject(transitionMap, map, map, f); + transitionTime += delta; + if (transitionTime >= transitionDuration) { + transitionSampler.paused = true; + transitionSampler = null; + } + } + else sampleAction(sampler, map); }; } else { - if (defaultSampler != null) { - deRegisterAction(DEFAULT_SAMPLER_ID); - defaultSampler = null; + if (transitionSampler != null) { + transitionSampler.paused = true; + transitionSampler = null; } + if (defaultSampler != null) defaultSampler.paused = true; + defaultSampler = null; updateAnimation = null; } } override public function update(delta: FastFloat) { + this.delta = delta; if (!object.visible || object.culled) return; #if lnx_debug @@ -98,7 +135,6 @@ class ObjectAnimation extends Animation { transformMap = initTransformMap(); super.update(delta); - if (defaultSampler != null) defaultSampler.paused = paused; if (paused) return; if (updateAnimation == null) return; if (!isSkinned) updateObjectAnimation(); @@ -160,11 +196,13 @@ class ObjectAnimation extends Animation { var ti = sampler.offset; //ti = ti < 0 ? 1 : ti; + var tj = ti + sign; + if (tj < 0 || tj >= track.frames.length) tj = ti; var t1 = track.frames[ti] * frameTime; - var t2 = track.frames[ti + sign] * frameTime; + var t2 = track.frames[tj] * frameTime; var v1 = track.values[ti]; - var v2 = track.values[ti + sign]; + var v2 = track.values[tj]; var value = interpolateLinear(t, t1, t2, v1, v2); @@ -202,6 +240,7 @@ class ObjectAnimation extends Animation { } inline function interpolateLinear(t: FastFloat, t1: FastFloat, t2: FastFloat, v1: FastFloat, v2: FastFloat): Null { + if (t2 == t1) return v1; var s = (t - t1) / (t2 - t1); return (1.0 - s) * v1 + s * v2; } diff --git a/leenkx/Sources/iron/object/Transform.hx b/leenkx/Sources/iron/object/Transform.hx index d4a7a1b2..01b0219a 100644 --- a/leenkx/Sources/iron/object/Transform.hx +++ b/leenkx/Sources/iron/object/Transform.hx @@ -131,13 +131,20 @@ class Transform { dscale = new Vec4().set(1, 1, 1); } + static var _cdLoc = new Vec4(); + static var _cdRot = new Quat(); + static var _cdScale = new Vec4(); + function composeDelta() { - // Delta transform - dloc.addvecs(loc, dloc); - dscale.addvecs(dscale, scale); - drot.fromEuler(_deulerX, _deulerY, _deulerZ); - drot.multquats(rot, drot); - local.compose(dloc, drot, dscale); + // Delta transform: additive location, multiplicative rotation and scale + _cdLoc.addvecs(loc, dloc); + _cdScale.x = scale.x * dscale.x; + _cdScale.y = scale.y * dscale.y; + _cdScale.z = scale.z * dscale.z; + _cdRot.fromEuler(_deulerX, _deulerY, _deulerZ); + _cdRot.multquats(_cdRot, drot); + _cdRot.multquats(rot, _cdRot); + local.compose(_cdLoc, _cdRot, _cdScale); } /** diff --git a/leenkx/Sources/iron/system/Time.hx b/leenkx/Sources/iron/system/Time.hx index ba502ca2..c7b5a915 100644 --- a/leenkx/Sources/iron/system/Time.hx +++ b/leenkx/Sources/iron/system/Time.hx @@ -38,14 +38,14 @@ class Time { static var normalModeLogged: Bool = false; #end - static var lastTime = 0.0; + static var lastTime = -1.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 lastRenderTime = -1.0; static var _renderDelta = 0.0; public static var renderDelta(get, never): Float; static function get_renderDelta(): Float { @@ -79,12 +79,14 @@ class Time { } #end - _delta = realTime() - lastTime; - lastTime = realTime(); + var now = realTime(); + _delta = lastTime < 0.0 ? 0.0 : now - lastTime; + lastTime = now; } public static function render() { - _renderDelta = realTime() - lastRenderTime; - lastRenderTime = realTime(); + var now = realTime(); + _renderDelta = lastRenderTime < 0.0 ? 0.0 : now - lastRenderTime; + lastRenderTime = now; } } diff --git a/leenkx/Sources/leenkx/logicnode/AnimActionNode.hx b/leenkx/Sources/leenkx/logicnode/AnimActionNode.hx index 7ec99e3c..073cb4a8 100644 --- a/leenkx/Sources/leenkx/logicnode/AnimActionNode.hx +++ b/leenkx/Sources/leenkx/logicnode/AnimActionNode.hx @@ -35,13 +35,13 @@ class AnimActionNode extends LogicNode { if(object.animation == null) { #if lnx_skin animationBone = object.getBoneAnimation(object.uid); - animationBone.registerAction(property0, sampler); + animationBone.registerAction(sampler.action, sampler); func = sampleBonaAction; #end } else{ animationObject = cast(object.animation, ObjectAnimation); - animationObject.registerAction(property0, sampler); + animationObject.registerAction(sampler.action, sampler); func = sampleObjectAction; } diff --git a/leenkx/Sources/leenkx/logicnode/BlendActionNode.hx b/leenkx/Sources/leenkx/logicnode/BlendActionNode.hx index 1215a273..a53589c3 100644 --- a/leenkx/Sources/leenkx/logicnode/BlendActionNode.hx +++ b/leenkx/Sources/leenkx/logicnode/BlendActionNode.hx @@ -61,14 +61,16 @@ class BlendActionNode extends LogicNode { return; } if(factor > 0.95) { - - inputs[2].get()(animMats); + var action2 = inputs[2].get(); + if (action2 != null) action2(animMats); else inputs[1].get()(animMats); return; } } inputs[1].get()(animMats); - inputs[2].get()(tempMats); + var action2 = inputs[2].get(); + if (action2 == null) return; + action2(tempMats); animationBone.blendAction(animMats, tempMats, animMats, factor, boneLayer); } diff --git a/leenkx/Sources/leenkx/logicnode/OnActionMarkerNode.hx b/leenkx/Sources/leenkx/logicnode/OnActionMarkerNode.hx index e629afa7..3e229875 100644 --- a/leenkx/Sources/leenkx/logicnode/OnActionMarkerNode.hx +++ b/leenkx/Sources/leenkx/logicnode/OnActionMarkerNode.hx @@ -7,6 +7,9 @@ class OnActionMarkerNode extends LogicNode { public var property0: String; public var property1: String; + var registeredSamplers: Array = []; + var registeredCallbacks: MapVoid> = new Map(); + public function new(tree: LogicTree) { super(tree); @@ -21,9 +24,33 @@ class OnActionMarkerNode extends LogicNode { assert(Error, object != null, "Object input cannot be null"); var animation = object.animation; if (animation == null) animation = object.getBoneAnimation(object.uid); - var action = animation.activeActions.get(actionID); - if(action == null) return; - animation.notifyOnMarker(action, marker, function() { runOutput(0); }); - tree.removeUpdate(init); + if (animation == null) return; + var active = animation.activeActions; + if (active == null) return; + + var i = registeredSamplers.length; + while (i-- > 0) { + var alive = false; + for (sampler in active) if (sampler == registeredSamplers[i]) { + alive = true; + break; + } + if (!alive) { + var s = registeredSamplers[i]; + animation.removeMarker(s, marker, registeredCallbacks.get(s)); + registeredCallbacks.remove(s); + registeredSamplers.splice(i, 1); + } + } + + for (key in active.keys()) { + var sampler = active.get(key); + if (actionID != "" && key != actionID && sampler.action != actionID) continue; + if (registeredSamplers.indexOf(sampler) != -1) continue; + registeredSamplers.push(sampler); + var cb = function() { runOutput(0); }; + registeredCallbacks.set(sampler, cb); + animation.notifyOnMarker(sampler, marker, cb); + } } } diff --git a/leenkx/Sources/leenkx/logicnode/PlayAnimationTreeNode.hx b/leenkx/Sources/leenkx/logicnode/PlayAnimationTreeNode.hx index 27c17e4f..e52e4520 100644 --- a/leenkx/Sources/leenkx/logicnode/PlayAnimationTreeNode.hx +++ b/leenkx/Sources/leenkx/logicnode/PlayAnimationTreeNode.hx @@ -27,11 +27,11 @@ class PlayAnimationTreeNode extends LogicNode { } function playAnim() { - if (init = false) return; + if (init == false) return; init = false; tree.removeUpdate(playAnim); - + var animation = object.animation; if(animation == null) { #if lnx_skin diff --git a/leenkx/Sources/leenkx/logicnode/RestartActionNode.hx b/leenkx/Sources/leenkx/logicnode/RestartActionNode.hx index e1c023a6..dcc1e581 100644 --- a/leenkx/Sources/leenkx/logicnode/RestartActionNode.hx +++ b/leenkx/Sources/leenkx/logicnode/RestartActionNode.hx @@ -16,8 +16,10 @@ class RestartActionNode extends LogicNode { var animation = object.animation; if (animation == null) animation = object.getBoneAnimation(object.uid); - if(animation.activeActions == null) return; - animation.activeActions.get(action).restartAction(); + if (animation == null || animation.activeActions == null) return; + var sampler = animation.activeActions.get(action); + if (sampler == null) return; + sampler.restartAction(); runOutput(0); } diff --git a/leenkx/Sources/leenkx/logicnode/SetActionPausedNode.hx b/leenkx/Sources/leenkx/logicnode/SetActionPausedNode.hx index e74e4a32..1428e5e5 100644 --- a/leenkx/Sources/leenkx/logicnode/SetActionPausedNode.hx +++ b/leenkx/Sources/leenkx/logicnode/SetActionPausedNode.hx @@ -19,8 +19,10 @@ class SetActionPausedNode extends LogicNode { var animation = object.animation; if (animation == null) animation = object.getBoneAnimation(object.uid); - if(animation.activeActions == null) return; - animation.activeActions.get(action).paused = paused; + if (animation == null || animation.activeActions == null) return; + var sampler = animation.activeActions.get(action); + if (sampler == null) return; + sampler.paused = paused; runOutput(0); } diff --git a/leenkx/Sources/leenkx/renderpath/RenderPathCreator.hx b/leenkx/Sources/leenkx/renderpath/RenderPathCreator.hx index 34ca8ca9..11fe15d8 100644 --- a/leenkx/Sources/leenkx/renderpath/RenderPathCreator.hx +++ b/leenkx/Sources/leenkx/renderpath/RenderPathCreator.hx @@ -1,4 +1,4 @@ -// Reference: https://github.com/leenkx3d/leenkx_docs/blob/master/dev/renderpath.md +// Reference: https://dev.leenkx.com/LeenkxTeam/LNXSDK/src/branch/main/api/src/manual/renderpath.md package leenkx.renderpath; import iron.RenderPath; diff --git a/leenkx/Sources/leenkx/renderpath/RenderPathForward.hx b/leenkx/Sources/leenkx/renderpath/RenderPathForward.hx index ca9b596d..f82fae8c 100644 --- a/leenkx/Sources/leenkx/renderpath/RenderPathForward.hx +++ b/leenkx/Sources/leenkx/renderpath/RenderPathForward.hx @@ -222,7 +222,7 @@ class RenderPathForward { } #end - #if ((rp_antialiasing == "SMAA") || (rp_antialiasing == "TAA") || (rp_ssr && !rp_ssr_half) || (rp_water) || (rp_depth_texture)) + #if ((rp_antialiasing == "SMAA") || (rp_antialiasing == "TAA") || (rp_fsr1 && rp_supersampling != 4) || (rp_ssr && !rp_ssr_half) || (rp_water) || (rp_depth_texture)) { var t = new RenderTargetRaw(); t.name = "bufa"; diff --git a/leenkx/Sources/leenkx/trait/NavAgent.hx b/leenkx/Sources/leenkx/trait/NavAgent.hx index 247adceb..90bb7ac6 100644 --- a/leenkx/Sources/leenkx/trait/NavAgent.hx +++ b/leenkx/Sources/leenkx/trait/NavAgent.hx @@ -139,10 +139,10 @@ class NavAgent extends Trait { if (forward.length() > 0.0001) { forward.normalize(); - var xAxis = new Vec4(1, 0, 0, 0); - xAxis.applyQuat(targetRot); + var yAxis = new Vec4(0, -1, 0, 0); + yAxis.applyQuat(targetRot); - var currForward = new Vec4().setFrom(xAxis); + var currForward = new Vec4().setFrom(yAxis); var twist = new Quat().fromTo(currForward, forward); diff --git a/leenkx/Sources/leenkx/trait/VehicleBody.hx b/leenkx/Sources/leenkx/trait/VehicleBody.hx index 30007e5e..39739ec1 100644 --- a/leenkx/Sources/leenkx/trait/VehicleBody.hx +++ b/leenkx/Sources/leenkx/trait/VehicleBody.hx @@ -263,8 +263,6 @@ class VehicleWheel { } } - #end - #elseif lnx_jolt typedef VehicleBody = leenkx.trait.physics.jolt.VehicleBody; diff --git a/leenkx/Sources/leenkx/trait/internal/LoadingScreen.hx b/leenkx/Sources/leenkx/trait/internal/LoadingScreen.hx index 1c13617f..3836bf84 100644 --- a/leenkx/Sources/leenkx/trait/internal/LoadingScreen.hx +++ b/leenkx/Sources/leenkx/trait/internal/LoadingScreen.hx @@ -4,41 +4,84 @@ package leenkx.trait.internal; class LoadingScreen { + public static var bgImageName = 'splash.png'; + public static var barColor: kha.Color = 0xff2ae0e0; + public static var bgColor: kha.Color = 0xff002a35; + public static var imageFit = 'Center'; + public static var showPercentage = true; + static var font: kha.Font = null; static var fontRequested: Bool = false; - static var randomColorR: Float = 0; - static var randomColorG: Float = 0; - static var randomColorB: Float = 0; - static var isInitialized = false; + static var bgImage: kha.Image = null; + static var bgRequested: Bool = false; public static function render(g: kha.graphics2.Graphics, assetsLoaded: Int, assetsTotal: Int) { - if (!isInitialized) { - randomColorR = Math.random(); - randomColorG = Math.random(); - randomColorB = Math.random(); - isInitialized = true; - } - var progress = assetsLoaded / assetsTotal; - var r = progress * randomColorR; - var gg = progress * randomColorG; - var b = progress * randomColorB; + final barHeight = 18; + var w = iron.App.w(); + var h = iron.App.h(); - g.color = kha.Color.fromFloats(r, gg, b, 1); - g.fillRect(0, iron.App.h() - 50, iron.App.w() / assetsTotal * assetsLoaded, 50); + g.color = bgColor; + g.fillRect(0, 0, w, h); - if (font == null && !fontRequested) { - fontRequested = true; - iron.data.Data.getFont('font_default.ttf', (f: kha.Font) -> { - font = f; + if (bgImageName != '' && bgImage == null && !bgRequested) { + bgRequested = true; + iron.data.Data.getImage(bgImageName, (i: kha.Image) -> { + bgImage = i; }); } - else if (font != null) { - g.font = font; - g.fontSize = 50; + if (bgImage != null) { g.color = 0xffffffff; - g.drawString(Math.floor(progress * 100) + '%', iron.App.w() / assetsTotal * assetsLoaded - 80, iron.App.h() - 50); + if (imageFit == 'Stretch') { + g.drawScaledImage(bgImage, 0, 0, w, h); + } + else { + var anchor = imageFit; + var dw: Float = bgImage.width; + var dh: Float = bgImage.height; + if (anchor.indexOf('Contain') == 0) { + var scale = Math.min(w / dw, h / dh); + dw *= scale; + dh *= scale; + anchor = anchor.length > 7 ? anchor.substr(7) : 'Center'; + } + var x = 0.0; + var y = 0.0; + switch (anchor) { + case 'TopCenter' | 'Center' | 'BottomCenter': + x = (w - dw) / 2; + case 'TopRight' | 'CenterRight' | 'BottomRight': + x = w - dw; + default: + } + switch (anchor) { + case 'CenterLeft' | 'Center' | 'CenterRight': + y = (h - dh) / 2; + case 'BottomLeft' | 'BottomCenter' | 'BottomRight': + y = h - dh; + default: + } + g.drawScaledImage(bgImage, x, y, dw, dh); + } + } + + g.color = barColor; + g.fillRect(0, h - barHeight, w / assetsTotal * assetsLoaded, barHeight); + + if (showPercentage) { + if (font == null && !fontRequested) { + fontRequested = true; + iron.data.Data.getFont('font_default.ttf', (f: kha.Font) -> { + font = f; + }); + } + else if (font != null) { + g.font = font; + g.fontSize = 30; + g.color = barColor; + g.drawString(Math.floor(progress * 100) + '%', w / assetsTotal * assetsLoaded - 60, h - barHeight - 34); + } } } } diff --git a/leenkx/blender/lnx/exporter.py b/leenkx/blender/lnx/exporter.py index 1a1c3515..41854763 100644 --- a/leenkx/blender/lnx/exporter.py +++ b/leenkx/blender/lnx/exporter.py @@ -2724,6 +2724,8 @@ class LeenkxExporter: self.extract_projection(o, proj) else: self.extract_ortho(o, proj) + if par_x == par_y: + o.pop('aspect', None) o['frustum_culling'] = objref.lnx_frustum_culling o['clear_color'] = self.get_camera_clear_color() @@ -3124,19 +3126,20 @@ class LeenkxExporter: def export_world(self): """Exports the world of the current scene.""" - for world in bpy.data.worlds: - if world is not None: - world_name = lnx.utils.safestr(lnx.utils.asset_name(world) if world.library else world.name) + world = self.scene.world - if world_name not in self.world_array: - self.world_array.append(world_name) - out_world = {'name': world_name} + if world is not None: + world_name = lnx.utils.safestr(lnx.utils.asset_name(world) if world.library else world.name) - self.post_export_world(world, out_world) - self.output['world_datas'].append(out_world) + if world_name not in self.world_array: + self.world_array.append(world_name) + out_world = {'name': world_name} - elif lnx.utils.get_rp().rp_background == 'World': - log.warn(f'Scene "{self.scene.name}" is missing a world, some render targets will not be cleared') + self.post_export_world(world, out_world) + self.output['world_datas'].append(out_world) + + elif lnx.utils.get_rp().rp_background == 'World': + log.warn(f'Scene "{self.scene.name}" is missing a world, some render targets will not be cleared') def export_objects(self, scene): """Exports all supported blender objects. @@ -3397,7 +3400,8 @@ class LeenkxExporter: else: self.extract_ortho(out_camera, proj) - out_camera['aspect'] = effective_aspect + if par_x != par_y: + out_camera['aspect'] = effective_aspect self.output['camera_datas'].append(out_camera) diff --git a/leenkx/blender/lnx/logicnode/logic/LN_merge.py b/leenkx/blender/lnx/logicnode/logic/LN_merge.py index 9b8bc3ec..69676328 100644 --- a/leenkx/blender/lnx/logicnode/logic/LN_merge.py +++ b/leenkx/blender/lnx/logicnode/logic/LN_merge.py @@ -79,7 +79,7 @@ class MergeNode(LnxLogicTreeNode): # Recreate all original inputs array_nodes[str(id(newnode))] = newnode for idx, input in enumerate(self.inputs): - bpy.ops.lnx.node_add_input('EXEC_DEFAULT', node_index=str(id(newnode)), socket_type='LnxNodeSocketAction') + newnode.add_input('LnxNodeSocketAction', f'Input {idx}') for link in input.links: node_tree.links.new(link.from_socket, newnode.inputs[idx]) diff --git a/leenkx/blender/lnx/logicnode/replacement.py b/leenkx/blender/lnx/logicnode/replacement.py index c01073af..38c2ad9f 100644 --- a/leenkx/blender/lnx/logicnode/replacement.py +++ b/leenkx/blender/lnx/logicnode/replacement.py @@ -278,7 +278,8 @@ def replace_all(): log.error(f'There were errors in the node update procedure, a detailed report has been written to {reportfile}') - bpy.ops.lnx.show_node_update_errors() + if not bpy.app.background: + bpy.ops.lnx.show_node_update_errors() def node_compat_sdk2108(): diff --git a/leenkx/blender/lnx/make_world.py b/leenkx/blender/lnx/make_world.py index fd89eaab..da80f662 100644 --- a/leenkx/blender/lnx/make_world.py +++ b/leenkx/blender/lnx/make_world.py @@ -232,8 +232,7 @@ 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 '_EnvCol' in world.world_defs or '_EnvClouds' in world.world_defs: - frag.add_uniform('float envmapStrength', link='_envmapStrength') + frag.add_uniform('float envmapStrength', link='_envmapStrength') # Clear background color if '_EnvCol' in world.world_defs: @@ -242,7 +241,6 @@ def build_node_tree(world: bpy.types.World, frag: Shader, vert: Shader, con: Sha else: if '_EnvTex' in world.world_defs and '_EnvLDR' in world.world_defs: frag.write('fragColor.rgb = pow(fragColor.rgb, vec3(2.2));') - frag.write('fragColor.rgb *= envmapStrength;') if '_EnvClouds' in world.world_defs: frag.write('if (pos.z > 0.0) fragColor.rgb = mix(fragColor.rgb, traceClouds(fragColor.rgb, pos), clamp(pos.z * 5.0, 0, 1));') diff --git a/leenkx/blender/lnx/material/make_decal.py b/leenkx/blender/lnx/material/make_decal.py index 007ca2bf..83b8532f 100644 --- a/leenkx/blender/lnx/material/make_decal.py +++ b/leenkx/blender/lnx/material/make_decal.py @@ -3,12 +3,14 @@ import bpy import lnx.material.cycles as cycles import lnx.material.mat_state as mat_state import lnx.material.make_finalize as make_finalize +import lnx.material.make_mesh as make_mesh import lnx.utils if lnx.is_reload(__name__): cycles = lnx.reload_module(cycles) mat_state = lnx.reload_module(mat_state) make_finalize = lnx.reload_module(make_finalize) + make_mesh = lnx.reload_module(make_mesh) lnx.utils = lnx.reload_module(lnx.utils) else: lnx.enable_reload(__name__) @@ -64,14 +66,8 @@ def make(context_id): frag.write_attrib(' if (abs(mpos.z) > 1.0) discard;') frag.write_attrib(' vec2 texCoord = mpos.xy * 0.5 + 0.5;') - frag.write('vec3 basecol;') - frag.write('float roughness;') - frag.write('float metallic;') - frag.write('float occlusion;') - frag.write('float specular;') - frag.write('float opacity;') - frag.write('vec3 emissionCol;') # Declared to prevent compiler errors, but decals currently don't output any emission - frag.write('float ior;') + # Declared to prevent compiler errors, but decals currently don't output any emission + make_mesh._write_material_attribs_default(frag, parse_opacity=True) cycles.parse(mat_state.nodes, con_decal, vert, frag, geom, tesc, tese) frag.write('n /= (abs(n.x) + abs(n.y) + abs(n.z));') diff --git a/leenkx/blender/lnx/material/make_depth.py b/leenkx/blender/lnx/material/make_depth.py index eafbc2b2..2d2ae08e 100644 --- a/leenkx/blender/lnx/material/make_depth.py +++ b/leenkx/blender/lnx/material/make_depth.py @@ -241,8 +241,7 @@ def make(context_id, rpasses, shadowmap=False, shadowmap_transparent=False): vert.add_out('vec4 wvpposition') vert.write('wvpposition = gl_Position;') frag.write('float depth = (wvpposition.z / wvpposition.w) * 0.5 + 0.5;') - frag.write('vec3 color = basecol;') - frag.write('color *= 1.0 - opacity;') + frag.write('vec3 color = mix(vec3(1.0), basecol, opacity);') frag.write('fragColor = vec4(color, depth);') if parse_opacity and not shadowmap_transparent: diff --git a/leenkx/blender/lnx/material/make_mesh.py b/leenkx/blender/lnx/material/make_mesh.py index 1fd031bf..61098b65 100644 --- a/leenkx/blender/lnx/material/make_mesh.py +++ b/leenkx/blender/lnx/material/make_mesh.py @@ -398,11 +398,14 @@ def make_forward_mobile(con_mesh): return is_shadows = '_ShadowMap' in wrd.world_defs + is_transparent_shadows = '_ShadowMapTransparent' in wrd.world_defs is_shadows_atlas = '_ShadowMapAtlas' in wrd.world_defs shadowmap_sun = 'shadowMap' + shadowmap_sun_tr = 'shadowMapTransparent' if is_shadows_atlas: is_single_atlas = '_SingleAtlas' in wrd.world_defs shadowmap_sun = 'shadowMapAtlasSun' if not is_single_atlas else 'shadowMapAtlas' + shadowmap_sun_tr = 'shadowMapAtlasSunTransparent' if not is_single_atlas else 'shadowMapAtlasTransparent' frag.add_uniform('vec2 smSizeUniform', '_shadowMapSize', included=True) frag.write('vec3 direct = vec3(0.0);') @@ -417,6 +420,8 @@ def make_forward_mobile(con_mesh): vert.write('lightPosition = LWVP * spos;') frag.add_uniform('bool receiveShadow') frag.add_uniform(f'sampler2DShadow {shadowmap_sun}') + if is_transparent_shadows: + frag.add_uniform(f'sampler2D {shadowmap_sun_tr}') frag.add_uniform('float shadowsBias', '_sunShadowsBias') frag.write('if (receiveShadow) {') @@ -427,14 +432,20 @@ def make_forward_mobile(con_mesh): if is_shadows_atlas: frag.add_uniform('vec4 tileBoundsSunArray[maxLights * shadowmapCascades]', '_tileBoundsSunArray', included=True) frag.write('tileBounds = tileBoundsSunArray[0];') - frag.write(f'svisibility = shadowTestCascade({shadowmap_sun}, eye, wposition + n * shadowsBias * 10, shadowsBias, opacity != 1.0);') + frag.write(f'svisibility = shadowTestCascade({shadowmap_sun},') + if is_transparent_shadows: + frag.write(f'{shadowmap_sun_tr},') + frag.write('eye, wposition + n * shadowsBias * 10, shadowsBias') + if is_transparent_shadows: + frag.write(', opacity != 1.0') + frag.write(');') else: frag.write('if (lightPosition.w > 0.0) {') frag.write(' vec3 lPos = lightPosition.xyz / lightPosition.w;') if '_Legacy' in wrd.world_defs: - frag.write(f' svisibility = float(texture({shadowmap_sun}, vec2(lPos.xy)).r > lPos.z - shadowsBias, opacity != 1.0);') + frag.write(f' svisibility = vec3(float(texture({shadowmap_sun}, vec2(lPos.xy)).r > lPos.z - shadowsBias));') else: - frag.write(f' svisibility = texture({shadowmap_sun}, vec3(lPos.xy, lPos.z - shadowsBias), opacity != 1.0).r;') + frag.write(f' svisibility = vec3(texture({shadowmap_sun}, vec3(lPos.xy, lPos.z - shadowsBias)).r);') frag.write('}') frag.write('}') # receiveShadow frag.write('direct += basecol * sdotNL * sunCol * svisibility;') diff --git a/leenkx/blender/lnx/material/make_particle.py b/leenkx/blender/lnx/material/make_particle.py index 6a306fe7..5ff6abcb 100644 --- a/leenkx/blender/lnx/material/make_particle.py +++ b/leenkx/blender/lnx/material/make_particle.py @@ -80,9 +80,6 @@ def write(vert, particle_info=None, shadowmap=False): vert.add_const('float', 'P_NORMAL_FACTOR', str(normal_factor)) - if (shadowmap and normal_factor != 0.0): - vert.add_out('vec3 N') - if ramp_el_len != 0: vert.add_const('float', 'P_SIZE_OVER_TIME_FACTOR', str(size_over_time_factor)) @@ -156,6 +153,7 @@ def write(vert, particle_info=None, shadowmap=False): vert.write('p_velocity.z += (fhash(gl_InstanceID + pd_random + 2 * pd[0][3]) * 2.0 / pd_size - 1.0 / pd_size) * pd[1][3];') if normal_factor != 0.0: + vert.add_uniform('mat3 N', '_normalMatrix') vert.write('if (P_NORMAL_FACTOR != 0.0) {') vert.write(' vec3 local_normal_dir = normalize(ipos);') vert.write(' vec3 world_emitter_normal = normalize(N * local_normal_dir);') diff --git a/leenkx/blender/lnx/node_utils.py b/leenkx/blender/lnx/node_utils.py index d7a45853..d0c589ee 100644 --- a/leenkx/blender/lnx/node_utils.py +++ b/leenkx/blender/lnx/node_utils.py @@ -123,6 +123,8 @@ def set_socket_default(socket: NodeSocket, value: Any): """Set the socket's default value if it exists.""" if isinstance(socket, lnx.logicnode.lnx_sockets.LnxCustomSocket): if socket.lnx_socket_type != 'NONE': + if socket.lnx_socket_type == 'INT' and isinstance(value, float): + value = int(value) socket.default_value_raw = value # Shader-type sockets don't have a default value diff --git a/leenkx/blender/lnx/props.py b/leenkx/blender/lnx/props.py index f96f4eca..ce237445 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 = '2026.9' +lnx_version = '2026.10' lnx_commit = '$Id: 6b2644d47db169cedd95593497cc283207d23a74 $' LNX_TORRENT_DEFAULT_TRACKERS = [ @@ -382,6 +382,27 @@ def init_properties(): ('Hashlink', 'Hashlink', 'Hashlink')], name="Runtime", description="Runtime to use when launching the game", default='Krom', update=assets.invalidate_shader_cache) bpy.types.World.lnx_loadscreen = BoolProperty(name="Loading Screen", description="Show asset loading progress on published builds", default=True) + bpy.types.World.lnx_loadscreen_mode = EnumProperty( + items=[('Image', 'Image', 'Background image with a progress bar'), + ('Simple', 'Simple', 'Background color with a progress bar')], + name="Loading Screen Mode", description="What the loading screen displays behind the progress bar", default='Image', update=assets.invalidate_compiler_cache) + bpy.types.World.lnx_loadscreen_bgcolor = FloatVectorProperty(name="Loading Background Color", description="Background color shown behind the loading bar and image", default=(0.0, 0.165, 0.208, 1.0), min=0.0, max=1.0, subtype='COLOR', size=4, update=assets.invalidate_compiler_cache) + bpy.types.World.lnx_loadscreen_image = StringProperty(name="Loading Screen Image", description="Background image shown behind the loading bar, leave as 'Default' to use the Leenkx logo wallpaper", default="Default", subtype="FILE_PATH", update=assets.invalidate_compiler_cache) + bpy.types.World.lnx_loadscreen_fit = EnumProperty( + items=[('TopLeft', 'Top Left', 'Draw the image at its native size anchored to the top left'), + ('TopCenter', 'Top Center', 'Draw the image at its native size anchored to the top center'), + ('TopRight', 'Top Right', 'Draw the image at its native size anchored to the top right'), + ('CenterLeft', 'Center Left', 'Draw the image at its native size anchored to the center left'), + ('Center', 'Center', 'Draw the image at its native size centered on screen'), + ('CenterRight', 'Center Right', 'Draw the image at its native size anchored to the center right'), + ('BottomLeft', 'Bottom Left', 'Draw the image at its native size anchored to the bottom left'), + ('BottomCenter', 'Bottom Center', 'Draw the image at its native size anchored to the bottom center'), + ('BottomRight', 'Bottom Right', 'Draw the image at its native size anchored to the bottom right'), + ('Contain', 'Contain', 'Scale the image to fit inside the screen, preserving aspect ratio'), + ('Stretch', 'Stretch to Fit', 'Stretch the image to fill the screen, ignoring aspect ratio')], + name="Image Fit", description="How the loading screen image is positioned on screen", default='Center', update=assets.invalidate_compiler_cache) + bpy.types.World.lnx_loadscreen_show_percentage = BoolProperty(name="Loading Screen Percentage", description="Draw the loading progress percentage above the progress bar", default=False, update=assets.invalidate_compiler_cache) + bpy.types.World.lnx_loadscreen_color = FloatVectorProperty(name="Loading Bar Color", description="Color of the loading progress bar and percentage text", default=(0.1647, 0.8784, 0.8784, 1.0), min=0.0, max=1.0, subtype='COLOR', size=4) bpy.types.World.lnx_vsync = BoolProperty(name="VSync", description="Vertical Synchronization", default=True, update=assets.invalidate_compiler_cache) bpy.types.World.lnx_dce = BoolProperty(name="DCE", description="Enable dead code elimination", default=False, update=assets.invalidate_compiler_cache) bpy.types.World.lnx_asset_compression = BoolProperty(name="Asset Compression", description="Enable scene data compression with LZ4. Warning: This will slow down export!", default=False, update=assets.invalidate_compiler_cache) @@ -763,8 +784,8 @@ def update_leenkx_world(): # This allows for seamless migration from earlier versions of Leenkx for rp in wrd.lnx_rplist: # TODO: deprecated if hasattr(rp, 'rp_gi') and rp.rp_gi != 'Off': - rp.rp_gi = 'Off' rp.rp_voxels = rp.rp_gi + rp.rp_gi = 'Off' # For some breaking changes we need to use a special update # routine first before regularly replacing nodes diff --git a/leenkx/blender/lnx/props_renderpath.py b/leenkx/blender/lnx/props_renderpath.py index 49618fee..29976bc1 100644 --- a/leenkx/blender/lnx/props_renderpath.py +++ b/leenkx/blender/lnx/props_renderpath.py @@ -168,7 +168,7 @@ def update_preset(self, context): rpdat.rp_compositornodes = True rpdat.rp_volumetriclight = False rpdat.rp_ssao = True - rpdat.rp_ssgi = True + rpdat.rp_ssgi = False rpdat.lnx_ssrs = True rpdat.lnx_micro_shadowing = True rpdat.rp_ssr = True diff --git a/leenkx/blender/lnx/props_ui.py b/leenkx/blender/lnx/props_ui.py index fe6efe87..864874f4 100644 --- a/leenkx/blender/lnx/props_ui.py +++ b/leenkx/blender/lnx/props_ui.py @@ -1263,7 +1263,6 @@ class LNX_PT_ProjectFlagsPanel(bpy.types.Panel): col = column_with_heading(layout, 'Runtime', align=True) col.prop(wrd, 'lnx_live_patch') col.prop(wrd, 'lnx_stream_scene') - col.prop(wrd, 'lnx_loadscreen') col.prop(wrd, 'lnx_webview') col.prop(wrd, 'lnx_write_config') @@ -1289,6 +1288,33 @@ class LNX_PT_ProjectFlagsPanel(bpy.types.Panel): col.prop(wrd, 'lnx_copy_override') col.operator('lnx.copy_to_bundled', icon='IMAGE_DATA') +class LNX_PT_ProjectFlagsLoadscreenPanel(bpy.types.Panel): + bl_label = "Loading Screen" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "LNX_PT_ProjectFlagsPanel" + + def draw_header(self, context): + wrd = bpy.data.worlds['Lnx'] + self.layout.prop(wrd, 'lnx_loadscreen', text='') + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + wrd = bpy.data.worlds['Lnx'] + col = layout.column() + col.enabled = wrd.lnx_loadscreen + col.prop(wrd, 'lnx_loadscreen_mode') + col.prop(wrd, 'lnx_loadscreen_bgcolor') + col.prop(wrd, 'lnx_loadscreen_color') + if wrd.lnx_loadscreen_mode == 'Image': + col.prop(wrd, 'lnx_loadscreen_image') + col.prop(wrd, 'lnx_loadscreen_fit') + col.prop(wrd, 'lnx_loadscreen_show_percentage') + class LNX_PT_ProjectFlagsDebugConsolePanel(bpy.types.Panel): bl_label = "Debug Console" bl_space_type = "PROPERTIES" @@ -3017,9 +3043,9 @@ class LNX_OT_UpdateFileSDK(bpy.types.Operator): wrd = bpy.data.worlds['Lnx'] # This allows for seamless migration from earlier versions of Leenkx for rp in wrd.lnx_rplist: # TODO: deprecated - if rp.rp_gi != 'Off': + if hasattr(rp, 'rp_gi') and rp.rp_gi != 'Off': + rp.rp_voxels = rp.rp_gi rp.rp_gi = 'Off' - rp.rp_voxelao = True # Replace deprecated nodes lnx.logicnode.replacement.replace_all() @@ -3265,6 +3291,7 @@ __REG_CLASSES = ( LNX_PT_LeenkxExporterWindowsSettingsPanel, LNX_PT_LeenkxProjectPanel, LNX_PT_ProjectFlagsPanel, + LNX_PT_ProjectFlagsLoadscreenPanel, LNX_PT_ProjectFlagsDebugConsolePanel, LNX_PT_ProjectWindowPanel, LNX_PT_ProjectModulesPanel, diff --git a/leenkx/blender/lnx/write_data.py b/leenkx/blender/lnx/write_data.py index 3d471782..18365083 100644 --- a/leenkx/blender/lnx/write_data.py +++ b/leenkx/blender/lnx/write_data.py @@ -539,6 +539,12 @@ let project = new Project('""" + lnx.utils.safesrc(wrd.lnx_project_name + '-' + if wrd.lnx_loadscreen: assets.add_khafile_def('lnx_loadscreen') + custom_loadscreen = os.path.isfile(lnx.utils.get_fp() + '/Sources/' + wrd.lnx_project_package + '/LoadingScreen.hx') + if wrd.lnx_loadscreen_mode == 'Image' or custom_loadscreen: + loadscreen_image = bpy.path.abspath(wrd.lnx_loadscreen_image) if wrd.lnx_loadscreen_image else '' + if loadscreen_image == '' or not os.path.isfile(loadscreen_image): + loadscreen_image = os.path.join(sdk_path, 'leenkx', 'blender', 'theme', 'splash.png') + khafile.write(add_assets(loadscreen_image.replace('\\', '/'), use_data_dir=use_data_dir, rel_path=do_relpath_sdk)) if wrd.lnx_winresize or state.target == 'html5': assets.add_khafile_def('lnx_resizable') @@ -742,6 +748,20 @@ class Main { 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;") f.write("\n" + content_indent + "leenkx.system.Starter.assets = " + str([os.path.basename(path) for path in asset_references if 'Bundled' in path]) + ";") + if loadscreen_class == 'leenkx.trait.internal.LoadingScreen': + if wrd.lnx_loadscreen_mode == 'Image': + li = bpy.path.abspath(wrd.lnx_loadscreen_image) if wrd.lnx_loadscreen_image else '' + if li == '' or not os.path.isfile(li): + li = os.path.join(lnx.utils.get_sdk_path(), 'leenkx', 'blender', 'theme', 'splash.png') + f.write("\n" + content_indent + loadscreen_class + ".bgImageName = '" + os.path.basename(li) + "';") + f.write("\n" + content_indent + loadscreen_class + ".imageFit = '" + wrd.lnx_loadscreen_fit + "';") + else: + f.write("\n" + content_indent + loadscreen_class + ".bgImageName = '';") + c = wrd.lnx_loadscreen_color + f.write("\n" + content_indent + loadscreen_class + ".barColor = " + hex(0xff000000 + (int(c[0] * 255) << 16) + (int(c[1] * 255) << 8) + int(c[2] * 255)) + ";") + c = wrd.lnx_loadscreen_bgcolor + f.write("\n" + content_indent + loadscreen_class + ".bgColor = " + hex(0xff000000 + (int(c[0] * 255) << 16) + (int(c[1] * 255) << 8) + int(c[2] * 255)) + ";") + f.write("\n" + content_indent + loadscreen_class + ".showPercentage = " + ('true' if wrd.lnx_loadscreen_show_percentage else 'false') + ";") scenes_list = [f"'{scene.name}'" for scene in bpy.data.scenes if getattr(scene, 'lnx_export', True)] scenes_array = "[" + ", ".join(scenes_list) + "]" @@ -819,7 +839,7 @@ def write_indexhtml(w, h, is_publish): """+html.escape( wrd.lnx_project_name)+""" """+icon+""" - + """) if rpdat.rp_stereo or wrd.lnx_winmode == 'Fullscreen': f.write(""" diff --git a/leenkx/blender/theme/Leenkx_Logo.png b/leenkx/blender/theme/Leenkx_Logo.png new file mode 100644 index 00000000..8579e189 Binary files /dev/null and b/leenkx/blender/theme/Leenkx_Logo.png differ