Merge pull request 'moisesjpelaez - FixedUpdate - Physics Improvements - Private Fields' (#58) from Onek8/LNXSDK:main into main

Reviewed-on: LeenkxTeam/LNXSDK#58
This commit is contained in:
2025-06-02 16:01:44 +00:00
24 changed files with 871 additions and 466 deletions

View File

@ -12,6 +12,7 @@ class App {
static var traitInits: Array<Void->Void> = []; static var traitInits: Array<Void->Void> = [];
static var traitUpdates: Array<Void->Void> = []; static var traitUpdates: Array<Void->Void> = [];
static var traitLateUpdates: Array<Void->Void> = []; static var traitLateUpdates: Array<Void->Void> = [];
static var traitFixedUpdates: Array<Void->Void> = [];
static var traitRenders: Array<kha.graphics4.Graphics->Void> = []; static var traitRenders: Array<kha.graphics4.Graphics->Void> = [];
static var traitRenders2D: Array<kha.graphics2.Graphics->Void> = []; static var traitRenders2D: Array<kha.graphics2.Graphics->Void> = [];
public static var framebuffer: kha.Framebuffer; public static var framebuffer: kha.Framebuffer;
@ -23,6 +24,8 @@ class App {
public static var renderPathTime: Float; public static var renderPathTime: Float;
public static var endFrameCallbacks: Array<Void->Void> = []; public static var endFrameCallbacks: Array<Void->Void> = [];
#end #end
static var last = 0.0;
static var time = 0.0;
static var lastw = -1; static var lastw = -1;
static var lasth = -1; static var lasth = -1;
public static var onResize: Void->Void = null; public static var onResize: Void->Void = null;
@ -34,13 +37,14 @@ class App {
function new(done: Void->Void) { function new(done: Void->Void) {
done(); done();
kha.System.notifyOnFrames(render); kha.System.notifyOnFrames(render);
kha.Scheduler.addTimeTask(update, 0, iron.system.Time.delta); kha.Scheduler.addTimeTask(update, 0, iron.system.Time.step);
} }
public static function reset() { public static function reset() {
traitInits = []; traitInits = [];
traitUpdates = []; traitUpdates = [];
traitLateUpdates = []; traitLateUpdates = [];
traitFixedUpdates = [];
traitRenders = []; traitRenders = [];
traitRenders2D = []; traitRenders2D = [];
if (onResets != null) for (f in onResets) f(); if (onResets != null) for (f in onResets) f();
@ -50,12 +54,22 @@ class App {
if (Scene.active == null || !Scene.active.ready) return; if (Scene.active == null || !Scene.active.ready) return;
if (pauseUpdates) return; if (pauseUpdates) return;
iron.system.Time.update();
#if lnx_debug #if lnx_debug
startTime = kha.Scheduler.realTime(); startTime = kha.Scheduler.realTime();
#end #end
Scene.active.updateFrame(); Scene.active.updateFrame();
time += iron.system.Time.delta;
while (time >= iron.system.Time.fixedStep) {
for (f in traitFixedUpdates) f();
time -= iron.system.Time.fixedStep;
}
var i = 0; var i = 0;
var l = traitUpdates.length; var l = traitUpdates.length;
while (i < l) { while (i < l) {
@ -106,7 +120,7 @@ class App {
var frame = frames[0]; var frame = frames[0];
framebuffer = frame; framebuffer = frame;
iron.system.Time.update(); iron.system.Time.render();
if (Scene.active == null || !Scene.active.ready) { if (Scene.active == null || !Scene.active.ready) {
render2D(frame); render2D(frame);
@ -172,6 +186,14 @@ class App {
traitLateUpdates.remove(f); traitLateUpdates.remove(f);
} }
public static function notifyOnFixedUpdate(f: Void->Void) {
traitFixedUpdates.push(f);
}
public static function removeFixedUpdate(f: Void->Void) {
traitFixedUpdates.remove(f);
}
public static function notifyOnRender(f: kha.graphics4.Graphics->Void) { public static function notifyOnRender(f: kha.graphics4.Graphics->Void) {
traitRenders.push(f); traitRenders.push(f);
} }

View File

@ -16,6 +16,7 @@ class Trait {
var _remove: Array<Void->Void> = null; var _remove: Array<Void->Void> = null;
var _update: Array<Void->Void> = null; var _update: Array<Void->Void> = null;
var _lateUpdate: Array<Void->Void> = null; var _lateUpdate: Array<Void->Void> = null;
var _fixedUpdate: Array<Void->Void> = null;
var _render: Array<kha.graphics4.Graphics->Void> = null; var _render: Array<kha.graphics4.Graphics->Void> = null;
var _render2D: Array<kha.graphics2.Graphics->Void> = null; var _render2D: Array<kha.graphics2.Graphics->Void> = null;
@ -87,6 +88,23 @@ class Trait {
App.removeLateUpdate(f); App.removeLateUpdate(f);
} }
/**
Add fixed game logic handler.
**/
public function notifyOnFixedUpdate(f: Void->Void) {
if (_fixedUpdate == null) _fixedUpdate = [];
_fixedUpdate.push(f);
App.notifyOnFixedUpdate(f);
}
/**
Remove fixed game logic handler.
**/
public function removeFixedUpdate(f: Void->Void) {
_fixedUpdate.remove(f);
App.removeFixedUpdate(f);
}
/** /**
Add render handler. Add render handler.
**/ **/

View File

@ -392,6 +392,8 @@ typedef TParticleData = {
#end #end
public var name: String; public var name: String;
public var type: Int; // 0 - Emitter, Hair public var type: Int; // 0 - Emitter, Hair
public var auto_start: Bool;
public var is_unique: Bool;
public var loop: Bool; public var loop: Bool;
public var count: Int; public var count: Int;
public var frame_start: FastFloat; public var frame_start: FastFloat;

View File

@ -159,9 +159,17 @@ class Animation {
if(markerEvents.get(sampler) != null){ if(markerEvents.get(sampler) != null){
for (i in 0...anim.marker_frames.length) { for (i in 0...anim.marker_frames.length) {
if (frameIndex == anim.marker_frames[i]) { if (frameIndex == anim.marker_frames[i]) {
var marketAct = markerEvents.get(sampler); var markerAct = markerEvents.get(sampler);
var ar = marketAct.get(anim.marker_names[i]); var ar = markerAct.get(anim.marker_names[i]);
if (ar != null) for (f in ar) f(); 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();
}
}
} }
} }
lastFrameIndex = frameIndex; lastFrameIndex = frameIndex;

View File

@ -172,6 +172,10 @@ class Object {
for (f in t._init) App.removeInit(f); for (f in t._init) App.removeInit(f);
t._init = null; t._init = null;
} }
if (t._fixedUpdate != null) {
for (f in t._fixedUpdate) App.removeFixedUpdate(f);
t._fixedUpdate = null;
}
if (t._update != null) { if (t._update != null) {
for (f in t._update) App.removeUpdate(f); for (f in t._update) App.removeUpdate(f);
t._update = null; t._update = null;

View File

@ -2,6 +2,7 @@ package iron.object;
#if lnx_particles #if lnx_particles
import kha.FastFloat;
import kha.graphics4.Usage; import kha.graphics4.Usage;
import kha.arrays.Float32Array; import kha.arrays.Float32Array;
import iron.data.Data; import iron.data.Data;
@ -16,39 +17,45 @@ import iron.math.Vec4;
class ParticleSystem { class ParticleSystem {
public var data: ParticleData; public var data: ParticleData;
public var speed = 1.0; public var speed = 1.0;
var currentSpeed = 0.0;
var particles: Array<Particle>; var particles: Array<Particle>;
var ready: Bool; var ready: Bool;
public var frameRate = 24; var frameRate = 24;
public var lifetime = 0.0; var lifetime = 0.0;
public var animtime = 0.0; var animtime = 0.0;
public var time = 0.0; var time = 0.0;
public var spawnRate = 0.0; var spawnRate = 0.0;
var looptime = 0.0;
var seed = 0; var seed = 0;
public var r: TParticleData; var r: TParticleData;
public var gx: Float; var gx: Float;
public var gy: Float; var gy: Float;
public var gz: Float; var gz: Float;
public var alignx: Float; var alignx: Float;
public var aligny: Float; var aligny: Float;
public var alignz: Float; var alignz: Float;
var dimx: Float; var dimx: Float;
var dimy: Float; var dimy: Float;
var tilesx: Int; var tilesx: Int;
var tilesy: Int; var tilesy: Int;
var tilesFramerate: Int; var tilesFramerate: Int;
public var count = 0; var count = 0;
public var lap = 0; var lap = 0;
public var lapTime = 0.0; var lapTime = 0.0;
var m = Mat4.identity(); var m = Mat4.identity();
var ownerLoc = new Vec4(); var ownerLoc = new Vec4();
var ownerRot = new Quat(); var ownerRot = new Quat();
var ownerScl = new Vec4(); var ownerScl = new Vec4();
var random = 0.0;
public function new(sceneName: String, pref: TParticleReference) { public function new(sceneName: String, pref: TParticleReference) {
seed = pref.seed; seed = pref.seed;
currentSpeed = speed;
speed = 0;
particles = []; particles = [];
ready = false; ready = false;
@ -65,34 +72,61 @@ class ParticleSystem {
gy = 0; gy = 0;
gz = -9.81 * r.weight_gravity; gz = -9.81 * r.weight_gravity;
} }
alignx = r.object_align_factor[0] / 2; alignx = r.object_align_factor[0];
aligny = r.object_align_factor[1] / 2; aligny = r.object_align_factor[1];
alignz = r.object_align_factor[2] / 2; alignz = r.object_align_factor[2];
looptime = (r.frame_end - r.frame_start) / frameRate;
lifetime = r.lifetime / frameRate; lifetime = r.lifetime / frameRate;
animtime = (r.frame_end - r.frame_start) / frameRate; animtime = r.loop ? looptime : looptime + lifetime;
spawnRate = ((r.frame_end - r.frame_start) / r.count) / frameRate; spawnRate = ((r.frame_end - r.frame_start) / r.count) / frameRate;
for (i in 0...r.count) { for (i in 0...r.count) {
var particle = new Particle(i); particles.push(new Particle(i));
particle.sr = 1 - Math.random() * r.size_random;
particles.push(particle);
} }
ready = true; 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() { public function pause() {
lifetime = 0; speed = 0;
} }
public function resume() { public function resume() {
lifetime = r.lifetime / frameRate; 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) { public function update(object: MeshObject, owner: MeshObject) {
if (!ready || object == null || speed == 0.0) return; if (!ready || object == null || speed == 0.0) return;
var prevLap = lap;
// Copy owner world transform but discard scale // Copy owner world transform but discard scale
owner.transform.world.decompose(ownerLoc, ownerRot, ownerScl); owner.transform.world.decompose(ownerLoc, ownerRot, ownerScl);
object.transform.loc = ownerLoc; object.transform.loc = ownerLoc;
@ -115,17 +149,21 @@ class ParticleSystem {
} }
// Animate // Animate
time += Time.delta * speed; time += Time.renderDelta * speed; // realDelta to renderDelta
lap = Std.int(time / animtime); lap = Std.int(time / animtime);
lapTime = time - lap * animtime; lapTime = time - lap * animtime;
count = Std.int(lapTime / spawnRate); count = Std.int(lapTime / spawnRate);
if (lap > prevLap && !r.loop) {
end();
}
updateGpu(object, owner); updateGpu(object, owner);
} }
public function getData(): Mat4 { public function getData(): Mat4 {
var hair = r.type == 1; var hair = r.type == 1;
m._00 = r.loop ? animtime : -animtime; m._00 = animtime;
m._01 = hair ? 1 / particles.length : spawnRate; m._01 = hair ? 1 / particles.length : spawnRate;
m._02 = hair ? 1 : lifetime; m._02 = hair ? 1 : lifetime;
m._03 = particles.length; m._03 = particles.length;
@ -133,9 +171,9 @@ class ParticleSystem {
m._11 = hair ? 0 : aligny; m._11 = hair ? 0 : aligny;
m._12 = hair ? 0 : alignz; m._12 = hair ? 0 : alignz;
m._13 = hair ? 0 : r.factor_random; m._13 = hair ? 0 : r.factor_random;
m._20 = hair ? 0 : gx * r.mass; m._20 = hair ? 0 : gx;
m._21 = hair ? 0 : gy * r.mass; m._21 = hair ? 0 : gy;
m._22 = hair ? 0 : gz * r.mass; m._22 = hair ? 0 : gz;
m._23 = hair ? 0 : r.lifetime_random; m._23 = hair ? 0 : r.lifetime_random;
m._30 = tilesx; m._30 = tilesx;
m._31 = tilesy; m._31 = tilesy;
@ -144,13 +182,25 @@ class ParticleSystem {
return m; 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, owner: MeshObject) { function updateGpu(object: MeshObject, owner: MeshObject) {
if (!object.data.geom.instanced) setupGeomGpu(object, owner); if (!object.data.geom.instanced) setupGeomGpu(object, owner);
// GPU particles transform is attached to owner object // GPU particles transform is attached to owner object
} }
public function setupGeomGpu(object: MeshObject, owner: MeshObject) { function setupGeomGpu(object: MeshObject, owner: MeshObject) {
var instancedData = new Float32Array(particles.length * 6); var instancedData = new Float32Array(particles.length * 3);
var i = 0; var i = 0;
var normFactor = 1 / 32767; // pa.values are not normalized var normFactor = 1 / 32767; // pa.values are not normalized
@ -169,10 +219,6 @@ class ParticleSystem {
instancedData.set(i, pa.values[j * pa.size ] * normFactor * scaleFactor.x); i++; 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 + 1] * normFactor * scaleFactor.y); i++;
instancedData.set(i, pa.values[j * pa.size + 2] * normFactor * scaleFactor.z); i++; instancedData.set(i, pa.values[j * pa.size + 2] * normFactor * scaleFactor.z); i++;
instancedData.set(i, p.sr); i++;
instancedData.set(i, p.sr); i++;
instancedData.set(i, p.sr); i++;
} }
case 1: // Face case 1: // Face
@ -196,10 +242,6 @@ class ParticleSystem {
instancedData.set(i, pos.x * normFactor * scaleFactor.x); i++; instancedData.set(i, pos.x * normFactor * scaleFactor.x); i++;
instancedData.set(i, pos.y * normFactor * scaleFactor.y); i++; instancedData.set(i, pos.y * normFactor * scaleFactor.y); i++;
instancedData.set(i, pos.z * normFactor * scaleFactor.z); i++; instancedData.set(i, pos.z * normFactor * scaleFactor.z); i++;
instancedData.set(i, p.sr); i++;
instancedData.set(i, p.sr); i++;
instancedData.set(i, p.sr); i++;
} }
case 2: // Volume case 2: // Volume
@ -210,13 +252,9 @@ class ParticleSystem {
instancedData.set(i, (Math.random() * 2.0 - 1.0) * scaleFactorVolume.x); i++; 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.y); i++;
instancedData.set(i, (Math.random() * 2.0 - 1.0) * scaleFactorVolume.z); i++; instancedData.set(i, (Math.random() * 2.0 - 1.0) * scaleFactorVolume.z); i++;
instancedData.set(i, p.sr); i++;
instancedData.set(i, p.sr); i++;
instancedData.set(i, p.sr); i++;
} }
} }
object.data.geom.setupInstanced(instancedData, 3, Usage.StaticUsage); object.data.geom.setupInstanced(instancedData, 1, Usage.StaticUsage);
} }
function fhash(n: Int): Float { function fhash(n: Int): Float {
@ -255,10 +293,11 @@ class ParticleSystem {
class Particle { class Particle {
public var i: Int; public var i: Int;
public var px = 0.0;
public var py = 0.0; public var x = 0.0;
public var pz = 0.0; public var y = 0.0;
public var sr = 1.0; // Size random public var z = 0.0;
public var cameraDistance: Float; public var cameraDistance: Float;
public function new(i: Int) { public function new(i: Int) {

View File

@ -80,7 +80,7 @@ class Tilesheet {
function update() { function update() {
if (!ready || paused || action.start >= action.end) return; if (!ready || paused || action.start >= action.end) return;
time += Time.realDelta; time += Time.renderDelta;
var frameTime = 1 / raw.framerate; var frameTime = 1 / raw.framerate;
var framesToAdvance = 0; var framesToAdvance = 0;

View File

@ -1109,6 +1109,26 @@ class Uniforms {
case "_texUnpack": { case "_texUnpack": {
f = texUnpack != null ? texUnpack : 1.0; f = texUnpack != null ? texUnpack : 1.0;
} }
#if lnx_particles
case "_particleSizeRandom": {
var mo = cast(object, MeshObject);
if (mo.particleOwner != null && mo.particleOwner.particleSystems != null) {
f = mo.particleOwner.particleSystems[mo.particleIndex].getSizeRandom();
}
}
case "_particleRandom": {
var mo = cast(object, MeshObject);
if (mo.particleOwner != null && mo.particleOwner.particleSystems != null) {
f = mo.particleOwner.particleSystems[mo.particleIndex].getRandom();
}
}
case "_particleSize": {
var mo = cast(object, MeshObject);
if (mo.particleOwner != null && mo.particleOwner.particleSystems != null) {
f = mo.particleOwner.particleSystems[mo.particleIndex].getSize();
}
}
#end
} }
if (f == null && externalFloatLinks != null) { if (f == null && externalFloatLinks != null) {

View File

@ -8,15 +8,23 @@ class Time {
return 1 / frequency; return 1 / frequency;
} }
public static var scale = 1.0; static var _fixedStep: Null<Float>;
public static var delta(get, never): Float; public static var fixedStep(get, never): Float;
static function get_delta(): Float { static function get_fixedStep(): Float {
if (frequency == null) initFrequency(); return _fixedStep;
return (1 / frequency) * scale;
} }
static var last = 0.0;
public static var realDelta = 0.0; public static function initFixedStep(value: Float = 1 / 60) {
_fixedStep = value;
}
static var lastTime = 0.0;
public static var delta = 0.0;
static var lastRenderTime = 0.0;
public static var renderDelta = 0.0;
public static inline function time(): Float { public static inline function time(): Float {
return kha.Scheduler.time(); return kha.Scheduler.time();
} }
@ -31,7 +39,12 @@ class Time {
} }
public static function update() { public static function update() {
realDelta = realTime() - last; delta = (realTime() - lastTime) * scale;
last = realTime(); lastTime = realTime();
}
public static function render() {
renderDelta = (realTime() - lastRenderTime) * scale;
lastRenderTime = realTime();
} }
} }

View File

@ -31,7 +31,7 @@ class AddParticleToObjectNode extends LogicNode {
var mobjTo = cast(objTo, iron.object.MeshObject); var mobjTo = cast(objTo, iron.object.MeshObject);
mobjTo.setupParticleSystem(iron.Scene.active.raw.name, {name: 'LnxPS', seed: 0, particle: psys.r.name}); mobjTo.setupParticleSystem(iron.Scene.active.raw.name, {name: 'LnxPS', seed: 0, particle: @:privateAccess psys.r.name});
mobjTo.render_emitter = inputs[4].get(); mobjTo.render_emitter = inputs[4].get();
@ -47,7 +47,7 @@ class AddParticleToObjectNode extends LogicNode {
var oslot: Int = mobjTo.particleSystems.length-1; var oslot: Int = mobjTo.particleSystems.length-1;
var opsys = mobjTo.particleSystems[oslot]; var opsys = mobjTo.particleSystems[oslot];
opsys.setupGeomGpu(mobjTo.particleChildren[oslot], mobjTo); @:privateAccess opsys.setupGeomGpu(mobjTo.particleChildren[oslot], mobjTo);
} else { } else {
var sceneName: String = inputs[1].get(); var sceneName: String = inputs[1].get();
@ -82,7 +82,7 @@ class AddParticleToObjectNode extends LogicNode {
var oslot: Int = mobjTo.particleSystems.length-1; var oslot: Int = mobjTo.particleSystems.length-1;
var opsys = mobjTo.particleSystems[oslot]; var opsys = mobjTo.particleSystems[oslot];
opsys.setupGeomGpu(mobjTo.particleChildren[oslot], mobjTo); @:privateAccess opsys.setupGeomGpu(mobjTo.particleChildren[oslot], mobjTo);
break; break;
} }

View File

@ -8,7 +8,7 @@ class GetFPSNode extends LogicNode {
override function get(from: Int): Dynamic { override function get(from: Int): Dynamic {
if (from == 0) { if (from == 0) {
var fps = Math.round(1 / iron.system.Time.realDelta); var fps = Math.round(1 / iron.system.Time.renderDelta);
if ((fps == Math.POSITIVE_INFINITY) || (fps == Math.NEGATIVE_INFINITY) || (Math.isNaN(fps))) { if ((fps == Math.POSITIVE_INFINITY) || (fps == Math.NEGATIVE_INFINITY) || (Math.isNaN(fps))) {
return 0; return 0;
} }

View File

@ -25,37 +25,37 @@ class GetParticleDataNode extends LogicNode {
return switch (from) { return switch (from) {
case 0: case 0:
psys.r.name; @:privateAccess psys.r.name;
case 1: case 1:
psys.r.particle_size; @:privateAccess psys.r.particle_size;
case 2: case 2:
psys.r.frame_start; @:privateAccess psys.r.frame_start;
case 3: case 3:
psys.r.frame_end; @:privateAccess psys.r.frame_end;
case 4: case 4:
psys.lifetime; @:privateAccess psys.lifetime;
case 5: case 5:
psys.r.lifetime; @:privateAccess psys.r.lifetime;
case 6: case 6:
psys.r.emit_from; @:privateAccess psys.r.emit_from;
case 7: case 7:
new iron.math.Vec3(psys.alignx*2, psys.aligny*2, psys.alignz*2); new iron.math.Vec3(@:privateAccess psys.alignx*2, @:privateAccess psys.aligny*2, @:privateAccess psys.alignz*2);
case 8: case 8:
psys.r.factor_random; @:privateAccess psys.r.factor_random;
case 9: case 9:
new iron.math.Vec3(psys.gx, psys.gy, psys.gz); new iron.math.Vec3(@:privateAccess psys.gx, @:privateAccess psys.gy, @:privateAccess psys.gz);
case 10: case 10:
psys.r.weight_gravity; @:privateAccess psys.r.weight_gravity;
case 11: case 11:
psys.speed; psys.speed;
case 12: case 12:
psys.time; @:privateAccess psys.time;
case 13: case 13:
psys.lap; @:privateAccess psys.lap;
case 14: case 14:
psys.lapTime; @:privateAccess psys.lapTime;
case 15: case 15:
psys.count; @:privateAccess psys.count;
default: default:
null; null;
} }

View File

@ -22,7 +22,7 @@ class GetParticleNode extends LogicNode {
var names: Array<String> = []; var names: Array<String> = [];
if (mo.particleSystems != null) if (mo.particleSystems != null)
for (psys in mo.particleSystems) for (psys in mo.particleSystems)
names.push(psys.r.name); names.push(@:privateAccess psys.r.name);
return names; return names;
case 1: case 1:
return mo.particleSystems != null ? mo.particleSystems.length : 0; return mo.particleSystems != null ? mo.particleSystems.length : 0;

View File

@ -33,7 +33,7 @@ class RemoveParticleFromObjectNode extends LogicNode {
if (property0 == 'Name'){ if (property0 == 'Name'){
var name: String = inputs[2].get(); var name: String = inputs[2].get();
for (i => psys in mo.particleSystems){ for (i => psys in mo.particleSystems){
if (psys.r.name == name){ slot = i; break; } if (@:privateAccess psys.r.name == name){ slot = i; break; }
} }
} }
else slot = inputs[2].get(); else slot = inputs[2].get();

View File

@ -24,43 +24,43 @@ class SetParticleDataNode extends LogicNode {
switch (property0) { switch (property0) {
case 'Particle Size': case 'Particle Size':
psys.r.particle_size = inputs[3].get(); @:privateAccess psys.r.particle_size = inputs[3].get();
case 'Frame Start': case 'Frame Start':
psys.r.frame_start = inputs[3].get(); @:privateAccess psys.r.frame_start = inputs[3].get();
psys.animtime = (psys.r.frame_end - psys.r.frame_start) / psys.frameRate; @:privateAccess psys.animtime = (@:privateAccess psys.r.frame_end - @:privateAccess psys.r.frame_start) / @:privateAccess psys.frameRate;
psys.spawnRate = ((psys.r.frame_end - psys.r.frame_start) / psys.count) / psys.frameRate; @:privateAccess psys.spawnRate = ((@:privateAccess psys.r.frame_end - @:privateAccess psys.r.frame_start) / @:privateAccess psys.count) / @:privateAccess psys.frameRate;
case 'Frame End': case 'Frame End':
psys.r.frame_end = inputs[3].get(); @:privateAccess psys.r.frame_end = inputs[3].get();
psys.animtime = (psys.r.frame_end - psys.r.frame_start) / psys.frameRate; @:privateAccess psys.animtime = (@:privateAccess psys.r.frame_end - @:privateAccess psys.r.frame_start) / @:privateAccess psys.frameRate;
psys.spawnRate = ((psys.r.frame_end - psys.r.frame_start) / psys.count) / psys.frameRate; @:privateAccess psys.spawnRate = ((@:privateAccess psys.r.frame_end - @:privateAccess psys.r.frame_start) / @:privateAccess psys.count) / @:privateAccess psys.frameRate;
case 'Lifetime': case 'Lifetime':
psys.lifetime = inputs[3].get() / psys.frameRate; @:privateAccess psys.lifetime = inputs[3].get() / @:privateAccess psys.frameRate;
case 'Lifetime Random': case 'Lifetime Random':
psys.r.lifetime_random = inputs[3].get(); @:privateAccess psys.r.lifetime_random = inputs[3].get();
case 'Emit From': case 'Emit From':
var emit_from: Int = inputs[3].get(); var emit_from: Int = inputs[3].get();
if (emit_from == 0 || emit_from == 1 || emit_from == 2) { if (emit_from == 0 || emit_from == 1 || emit_from == 2) {
psys.r.emit_from = emit_from; @:privateAccess psys.r.emit_from = emit_from;
psys.setupGeomGpu(mo.particleChildren != null ? mo.particleChildren[slot] : cast(iron.Scene.active.getChild(psys.data.raw.instance_object), iron.object.MeshObject), mo); @:privateAccess psys.setupGeomGpu(mo.particleChildren != null ? mo.particleChildren[slot] : cast(iron.Scene.active.getChild(psys.data.raw.instance_object), iron.object.MeshObject), mo);
} }
case 'Velocity': case 'Velocity':
var vel: iron.math.Vec3 = inputs[3].get(); var vel: iron.math.Vec3 = inputs[3].get();
psys.alignx = vel.x / 2; @:privateAccess psys.alignx = vel.x / 2;
psys.aligny = vel.y / 2; @:privateAccess psys.aligny = vel.y / 2;
psys.alignz = vel.z / 2; @:privateAccess psys.alignz = vel.z / 2;
case 'Velocity Random': case 'Velocity Random':
psys.r.factor_random = inputs[3].get(); psys.r.factor_random = inputs[3].get();
case 'Weight Gravity': case 'Weight Gravity':
psys.r.weight_gravity = inputs[3].get(); psys.r.weight_gravity = inputs[3].get();
if (iron.Scene.active.raw.gravity != null) { if (iron.Scene.active.raw.gravity != null) {
psys.gx = iron.Scene.active.raw.gravity[0] * psys.r.weight_gravity; @:privateAccess psys.gx = iron.Scene.active.raw.gravity[0] * @:privateAccess psys.r.weight_gravity;
psys.gy = iron.Scene.active.raw.gravity[1] * psys.r.weight_gravity; @:privateAccess psys.gy = iron.Scene.active.raw.gravity[1] * @:privateAccess psys.r.weight_gravity;
psys.gz = iron.Scene.active.raw.gravity[2] * psys.r.weight_gravity; @:privateAccess psys.gz = iron.Scene.active.raw.gravity[2] * @:privateAccess psys.r.weight_gravity;
} }
else { else {
psys.gx = 0; @:privateAccess psys.gx = 0;
psys.gy = 0; @:privateAccess psys.gy = 0;
psys.gz = -9.81 * psys.r.weight_gravity; @:privateAccess psys.gz = -9.81 * @:privateAccess psys.r.weight_gravity;
} }
case 'Speed': case 'Speed':
psys.speed = inputs[3].get(); psys.speed = inputs[3].get();

View File

@ -101,7 +101,7 @@ class PhysicsWorld extends Trait {
public function new(timeScale = 1.0, maxSteps = 10, solverIterations = 10, debugDrawMode: DebugDrawMode = NoDebug) { public function new(timeScale = 1.0, maxSteps = 10, solverIterations = 10, fixedStep = 1 / 60, debugDrawMode: DebugDrawMode = NoDebug) {
super(); super();
if (nullvec) { if (nullvec) {
@ -120,6 +120,7 @@ class PhysicsWorld extends Trait {
this.timeScale = timeScale; this.timeScale = timeScale;
this.maxSteps = maxSteps; this.maxSteps = maxSteps;
this.solverIterations = solverIterations; this.solverIterations = solverIterations;
Time.initFixedStep(fixedStep);
// First scene // First scene
if (active == null) { if (active == null) {
@ -136,9 +137,9 @@ class PhysicsWorld extends Trait {
conMap = new Map(); conMap = new Map();
active = this; active = this;
// Ensure physics are updated first in the lateUpdate list // Ensure physics are updated first in the fixedUpdate list
_lateUpdate = [lateUpdate]; _fixedUpdate = [fixedUpdate];
@:privateAccess iron.App.traitLateUpdates.insert(0, lateUpdate); @:privateAccess iron.App.traitFixedUpdates.insert(0, fixedUpdate);
setDebugDrawMode(debugDrawMode); setDebugDrawMode(debugDrawMode);
@ -298,8 +299,8 @@ class PhysicsWorld extends Trait {
return rb; return rb;
} }
function lateUpdate() { function fixedUpdate() {
var t = Time.delta * timeScale; var t = Time.fixedStep * timeScale * Time.scale;
if (t == 0.0) return; // Simulation paused if (t == 0.0) return; // Simulation paused
#if lnx_debug #if lnx_debug
@ -308,13 +309,10 @@ class PhysicsWorld extends Trait {
if (preUpdates != null) for (f in preUpdates) f(); if (preUpdates != null) for (f in preUpdates) f();
//Bullet physics fixed timescale
var fixedTime = 1.0 / 60;
//This condition must be satisfied to not loose time //This condition must be satisfied to not loose time
var currMaxSteps = t < (fixedTime * maxSteps) ? maxSteps : 1; var currMaxSteps = t < (Time.fixedStep * maxSteps) ? maxSteps : 1;
world.stepSimulation(t, currMaxSteps, fixedTime); world.stepSimulation(t, currMaxSteps, Time.fixedStep);
updateContacts(); updateContacts();
for (rb in rbMap) @:privateAccess rb.physicsUpdate(); for (rb in rbMap) @:privateAccess rb.physicsUpdate();

View File

@ -2,11 +2,13 @@ package leenkx.trait.physics.bullet;
#if lnx_bullet #if lnx_bullet
import leenkx.math.Helper;
import iron.data.MeshData;
import iron.math.Vec4; import iron.math.Vec4;
import iron.math.Quat; import iron.math.Quat;
import iron.object.Transform; import iron.object.Transform;
import iron.object.MeshObject; import iron.object.MeshObject;
import iron.data.MeshData; import iron.system.Time;
/** /**
RigidBody is used to allow objects to interact with Physics in your game including collisions and gravity. RigidBody is used to allow objects to interact with Physics in your game including collisions and gravity.
@ -76,6 +78,14 @@ class RigidBody extends iron.Trait {
static var triangleMeshCache = new Map<MeshData, bullet.Bt.TriangleMesh>(); static var triangleMeshCache = new Map<MeshData, bullet.Bt.TriangleMesh>();
static var usersCache = new Map<MeshData, Int>(); static var usersCache = new Map<MeshData, Int>();
// 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);
var prevRot: bullet.Bt.Quaternion = new bullet.Bt.Quaternion(0, 0, 0, 1);
public function new(shape = Shape.Box, mass = 1.0, friction = 0.5, restitution = 0.0, group = 1, mask = 1, public function new(shape = Shape.Box, mass = 1.0, friction = 0.5, restitution = 0.0, group = 1, mask = 1,
params: RigidBodyParams = null, flags: RigidBodyFlags = null) { params: RigidBodyParams = null, flags: RigidBodyFlags = null) {
super(); super();
@ -85,7 +95,7 @@ class RigidBody extends iron.Trait {
vec1 = new bullet.Bt.Vector3(0, 0, 0); vec1 = new bullet.Bt.Vector3(0, 0, 0);
vec2 = new bullet.Bt.Vector3(0, 0, 0); vec2 = new bullet.Bt.Vector3(0, 0, 0);
vec3 = new bullet.Bt.Vector3(0, 0, 0); vec3 = new bullet.Bt.Vector3(0, 0, 0);
quat1 = new bullet.Bt.Quaternion(0, 0, 0, 0); quat1 = new bullet.Bt.Quaternion(0, 0, 0, 1);
trans1 = new bullet.Bt.Transform(); trans1 = new bullet.Bt.Transform();
trans2 = new bullet.Bt.Transform(); trans2 = new bullet.Bt.Transform();
} }
@ -117,6 +127,7 @@ class RigidBody extends iron.Trait {
animated: false, animated: false,
trigger: false, trigger: false,
ccd: false, ccd: false,
interpolate: false,
staticObj: false, staticObj: false,
useDeactivation: true useDeactivation: true
}; };
@ -131,6 +142,7 @@ class RigidBody extends iron.Trait {
this.animated = flags.animated; this.animated = flags.animated;
this.trigger = flags.trigger; this.trigger = flags.trigger;
this.ccd = flags.ccd; this.ccd = flags.ccd;
this.interpolate = flags.interpolate;
this.staticObj = flags.staticObj; this.staticObj = flags.staticObj;
this.useDeactivation = flags.useDeactivation; this.useDeactivation = flags.useDeactivation;
@ -153,6 +165,7 @@ class RigidBody extends iron.Trait {
if (!Std.isOfType(object, MeshObject)) return; // No mesh data if (!Std.isOfType(object, MeshObject)) return; // No mesh data
transform = object.transform; transform = object.transform;
transform.buildMatrix();
physics = leenkx.trait.physics.PhysicsWorld.active; physics = leenkx.trait.physics.PhysicsWorld.active;
if (shape == Shape.Box) { if (shape == Shape.Box) {
@ -244,6 +257,9 @@ class RigidBody extends iron.Trait {
quat1.setValue(quat.x, quat.y, quat.z, quat.w); quat1.setValue(quat.x, quat.y, quat.z, quat.w);
trans1.setRotation(quat1); trans1.setRotation(quat1);
currentPos.setValue(vec1.x(), vec1.y(), vec1.z());
currentRot.setValue(quat.x, quat.y, quat.z, quat.w);
var centerOfMassOffset = trans2; var centerOfMassOffset = trans2;
centerOfMassOffset.setIdentity(); centerOfMassOffset.setIdentity();
motionState = new bullet.Bt.DefaultMotionState(trans1, centerOfMassOffset); motionState = new bullet.Bt.DefaultMotionState(trans1, centerOfMassOffset);
@ -307,6 +323,7 @@ class RigidBody extends iron.Trait {
physics.addRigidBody(this); physics.addRigidBody(this);
notifyOnRemove(removeFromWorld); notifyOnRemove(removeFromWorld);
if (!animated) notifyOnUpdate(update);
if (onReady != null) onReady(); if (onReady != null) onReady();
@ -317,26 +334,71 @@ class RigidBody extends iron.Trait {
#end #end
} }
function physicsUpdate() {
if (!ready) return;
if (animated) {
syncTransform();
}
else {
var trans = body.getWorldTransform();
var p = trans.getOrigin();
var q = trans.getRotation();
transform.loc.set(p.x(), p.y(), p.z()); function update() {
transform.rot.set(q.x(), q.y(), q.z(), q.w()); if (interpolate) {
time += Time.delta;
while (time >= Time.fixedStep) {
time -= Time.fixedStep;
}
var t: Float = time / Time.fixedStep;
t = Helper.clamp(t, 0, 1);
var tx: Float = prevPos.x() * (1.0 - t) + currentPos.x() * t;
var ty: Float = prevPos.y() * (1.0 - t) + currentPos.y() * t;
var tz: Float = prevPos.z() * (1.0 - t) + currentPos.z() * t;
var tRot: bullet.Bt.Quaternion = nlerp(prevRot, currentRot, t);
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) { if (object.parent != null) {
var ptransform = object.parent.transform; var ptransform = object.parent.transform;
transform.loc.x -= ptransform.worldx(); transform.loc.x -= ptransform.worldx();
transform.loc.y -= ptransform.worldy(); transform.loc.y -= ptransform.worldy();
transform.loc.z -= ptransform.worldz(); transform.loc.z -= ptransform.worldz();
} }
transform.clearDelta();
transform.buildMatrix(); transform.buildMatrix();
}
function nlerp(q1: bullet.Bt.Quaternion, q2: bullet.Bt.Quaternion, t: Float): bullet.Bt.Quaternion {
var dot = q1.x() * q2.x() + q1.y() * q2.y() + q1.z() * q2.z() + q1.w() * q2.w();
var _q2 = dot < 0 ? new bullet.Bt.Quaternion(-q2.x(), -q2.y(), -q2.z(), -q2.w()) : q2;
var x = q1.x() * (1.0 - t) + _q2.x() * t;
var y = q1.y() * (1.0 - t) + _q2.y() * t;
var z = q1.z() * (1.0 - t) + _q2.z() * t;
var w = q1.w() * (1.0 - t) + _q2.w() * t;
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;
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 #if hl
p.delete(); p.delete();
@ -689,6 +751,7 @@ typedef RigidBodyFlags = {
var animated: Bool; var animated: Bool;
var trigger: Bool; var trigger: Bool;
var ccd: Bool; var ccd: Bool;
var interpolate: Bool;
var staticObj: Bool; var staticObj: Bool;
var useDeactivation: Bool; var useDeactivation: Bool;
} }

View File

@ -2297,6 +2297,8 @@ class LeenkxExporter:
out_particlesys = { out_particlesys = {
'name': particleRef[1]["structName"], 'name': particleRef[1]["structName"],
'type': 0 if psettings.type == 'EMITTER' else 1, # HAIR 'type': 0 if psettings.type == 'EMITTER' else 1, # HAIR
'auto_start': psettings.lnx_auto_start,
'is_unique': psettings.lnx_is_unique,
'loop': psettings.lnx_loop, 'loop': psettings.lnx_loop,
# Emission # Emission
'count': int(psettings.count * psettings.lnx_count_mult), 'count': int(psettings.count * psettings.lnx_count_mult),
@ -2813,6 +2815,7 @@ class LeenkxExporter:
body_flags['animated'] = rb.kinematic body_flags['animated'] = rb.kinematic
body_flags['trigger'] = bobject.lnx_rb_trigger body_flags['trigger'] = bobject.lnx_rb_trigger
body_flags['ccd'] = bobject.lnx_rb_ccd body_flags['ccd'] = bobject.lnx_rb_ccd
body_flags['interpolate'] = bobject.lnx_rb_interpolate
body_flags['staticObj'] = is_static body_flags['staticObj'] = is_static
body_flags['useDeactivation'] = rb.use_deactivation 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_params))
@ -3037,7 +3040,7 @@ class LeenkxExporter:
rbw = self.scene.rigidbody_world rbw = self.scene.rigidbody_world
if rbw is not None and rbw.enabled: if rbw is not None and rbw.enabled:
out_trait['parameters'] = [str(rbw.time_scale), str(rbw.substeps_per_frame), str(rbw.solver_iterations)] out_trait['parameters'] = [str(rbw.time_scale), str(rbw.substeps_per_frame), str(rbw.solver_iterations), str(wrd.lnx_physics_fixed_step)]
if phys_pkg == 'bullet' or phys_pkg == 'oimo': if phys_pkg == 'bullet' or phys_pkg == 'oimo':
debug_draw_mode = 1 if wrd.lnx_physics_dbg_draw_wireframe else 0 debug_draw_mode = 1 if wrd.lnx_physics_dbg_draw_wireframe else 0

View File

@ -87,6 +87,7 @@ def on_operator_post(operator_id: str) -> None:
target_obj.lnx_rb_trigger = source_obj.lnx_rb_trigger target_obj.lnx_rb_trigger = source_obj.lnx_rb_trigger
target_obj.lnx_rb_deactivation_time = source_obj.lnx_rb_deactivation_time target_obj.lnx_rb_deactivation_time = source_obj.lnx_rb_deactivation_time
target_obj.lnx_rb_ccd = source_obj.lnx_rb_ccd target_obj.lnx_rb_ccd = source_obj.lnx_rb_ccd
target_obj.lnx_rb_interpolate = source_obj.lnx_rb_interpolate
target_obj.lnx_rb_collision_filter_mask = source_obj.lnx_rb_collision_filter_mask target_obj.lnx_rb_collision_filter_mask = source_obj.lnx_rb_collision_filter_mask
elif operator_id == "NODE_OT_new_node_tree": elif operator_id == "NODE_OT_new_node_tree":

View File

@ -82,25 +82,34 @@ 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]: 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] 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) 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 interp = node.color_ramp.interpolation
elems = node.color_ramp.elements elems = node.color_ramp.elements
if len(elems) == 1: if len(elems) == 1:
return c.to_vec3(elems[0].color) 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
# Write color array name_prefix = c.node_name(node.name).upper()
# The last entry is included twice so that the interpolation
# between indices works (no out of bounds error) if alpha_out:
cols_var = c.node_name(node.name).upper() + '_COLS' cols_var = name_prefix + '_ALPHAS'
else:
cols_var = name_prefix + '_COLS'
if state.current_pass == ParserPass.REGULAR: 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 = ', '.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]})' 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) state.curshader.add_const("vec3", cols_var, cols_entries, array_size=len(elems) + 1)
@ -121,21 +130,22 @@ def parse_valtorgb(node: bpy.types.ShaderNodeValToRGB, out_socket: bpy.types.Nod
# Linear interpolation # Linear interpolation
else: else:
# Write factor array # Write factor array - same for both color and alpha
facs_var = c.node_name(node.name).upper() + '_FACS' facs_var = name_prefix + '_FACS'
if state.current_pass == ParserPass.REGULAR: if state.current_pass == ParserPass.REGULAR:
facs_entries = ', '.join(str(elem.position) for elem in elems) facs_entries = ', '.join(str(elem.position) for elem in elems)
# Add one more entry at the rightmost position so that the # Add one more entry at the rightmost position to avoid out of bounds access
# interpolation between indices works (no out of bounds error)
facs_entries += ', 1.0' facs_entries += ', 1.0'
state.curshader.add_const("float", facs_var, facs_entries, array_size=len(elems) + 1) state.curshader.add_const("float", facs_var, facs_entries, array_size=len(elems) + 1)
# Mix color # Calculation for interpolation position
prev_stop_fac = f'{facs_var}[{index_var}]' prev_stop_fac = f'{facs_var}[{index_var}]'
next_stop_fac = f'{facs_var}[{index_var} + 1]' next_stop_fac = f'{facs_var}[{index_var} + 1]'
prev_stop_col = f'{cols_var}[{index_var}]' prev_stop_col = f'{cols_var}[{index_var}]'
next_stop_col = f'{cols_var}[{index_var} + 1]' 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}))' 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))' return f'mix({prev_stop_col}, {next_stop_col}, max({rel_pos}, 0.0))'
if bpy.app.version > (3, 2, 0): if bpy.app.version > (3, 2, 0):

View File

@ -1,3 +1,4 @@
import bpy
import lnx.utils import lnx.utils
import lnx.material.mat_state as mat_state import lnx.material.mat_state as mat_state
@ -10,6 +11,48 @@ else:
def write(vert, particle_info=None, shadowmap=False): def write(vert, particle_info=None, shadowmap=False):
ramp_el_len = 0
ramp_positions = []
ramp_colors_b = []
size_over_time_factor = 0
use_rotations = False
rotation_mode = 'NONE'
rotation_factor_random = 0
phase_factor = 0
phase_factor_random = 0
for obj in bpy.data.objects:
for psys in obj.particle_systems:
psettings = psys.settings
if psettings.instance_object:
if psettings.instance_object.active_material:
# FIXME: Different particle systems may share the same particle object. This ideally should check the correct `ParticleSystem` using an id or name in the particle's object material.
if psettings.instance_object.active_material.name.replace(".", "_") == vert.context.matname:
# Rotation data
use_rotations = psettings.use_rotations
rotation_mode = psettings.rotation_mode
rotation_factor_random = psettings.rotation_factor_random
phase_factor = psettings.phase_factor
phase_factor_random = psettings.phase_factor_random
# Texture slots data
if psettings.texture_slots and len(psettings.texture_slots.items()) != 0:
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.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:
ramp_positions.append(element.position)
ramp_colors_b.append(element.color[2])
size_over_time_factor = tex_slot.size_factor
break
# Outs # Outs
out_index = True if particle_info != None and particle_info['index'] else False out_index = True if particle_info != None and particle_info['index'] else False
out_age = True if particle_info != None and particle_info['age'] else False out_age = True if particle_info != None and particle_info['age'] else False
@ -19,19 +62,50 @@ def write(vert, particle_info=None, shadowmap=False):
out_velocity = True if particle_info != None and particle_info['velocity'] else False out_velocity = True if particle_info != None and particle_info['velocity'] else False
out_angular_velocity = True if particle_info != None and particle_info['angular_velocity'] else False out_angular_velocity = True if particle_info != None and particle_info['angular_velocity'] else False
# Force Leenkx to create a new shader per material ID
vert.write(f'#ifdef PARTICLE_ID_{vert.context.material.lnx_material_id}')
vert.write('#endif')
vert.add_uniform('mat4 pd', '_particleData') vert.add_uniform('mat4 pd', '_particleData')
vert.add_uniform('float pd_size_random', '_particleSizeRandom')
vert.add_uniform('float pd_random', '_particleRandom')
vert.add_uniform('float pd_size', '_particleSize')
if ramp_el_len != 0:
vert.add_const('float', 'P_SIZE_OVER_TIME_FACTOR', str(size_over_time_factor))
for i in range(ramp_el_len):
vert.add_const('float', f'P_RAMP_POSITION_{i}', str(ramp_positions[i]))
vert.add_const('float', f'P_RAMP_COLOR_B_{i}', str(ramp_colors_b[i]))
str_tex_hash = "float fhash(float n) { return fract(sin(n) * 43758.5453); }\n" str_tex_hash = "float fhash(float n) { return fract(sin(n) * 43758.5453); }\n"
vert.add_function(str_tex_hash) vert.add_function(str_tex_hash)
if (ramp_el_len != 0):
str_ramp_scale = "float get_ramp_scale(float age) {\n"
for i in range(ramp_el_len):
if i == 0:
str_ramp_scale += f"if (age <= P_RAMP_POSITION_{i + 1})"
elif i == ramp_el_len - 1:
str_ramp_scale += f"return P_RAMP_COLOR_B_{ramp_el_len - 1};"
break
else:
str_ramp_scale += f"else if (age <= P_RAMP_POSITION_{i + 1})"
str_ramp_scale += f""" {{
float t = (age - P_RAMP_POSITION_{i}) / (P_RAMP_POSITION_{i + 1} - P_RAMP_POSITION_{i});
return mix(P_RAMP_COLOR_B_{i}, P_RAMP_COLOR_B_{i + 1}, t);
}}
"""
str_ramp_scale += "}\n"
vert.add_function(str_ramp_scale)
prep = 'float ' prep = 'float '
if out_age: if out_age:
prep = '' prep = ''
vert.add_out('float p_age') vert.add_out('float p_age')
# var p_age = lapTime - p.i * spawnRate # var p_age = lapTime - p.i * spawnRate
vert.write(prep + 'p_age = pd[3][3] - gl_InstanceID * pd[0][1];') vert.write(prep + 'p_age = pd[3][3] - gl_InstanceID * pd[0][1];')
# p_age -= p_age * fhash(i) * r.lifetime_random;
vert.write('p_age -= p_age * fhash(gl_InstanceID) * pd[2][3];')
# Loop # Loop
# pd[0][0] - animtime, loop stored in sign # pd[0][0] - animtime, loop stored in sign
@ -43,13 +117,18 @@ def write(vert, particle_info=None, shadowmap=False):
if out_lifetime: if out_lifetime:
prep = '' prep = ''
vert.add_out('float p_lifetime') vert.add_out('float p_lifetime')
vert.write(prep + 'p_lifetime = pd[0][2];') vert.write(prep + 'p_lifetime = pd[0][2] * (1 - (fhash(gl_InstanceID + 4 * pd[0][3] + pd_random) * pd[2][3]));')
# clip with nan # clip with nan
vert.write('if (p_age < 0 || p_age > p_lifetime) {') vert.write('if (p_age < 0 || p_age > p_lifetime) {')
vert.write(' gl_Position /= 0.0;') vert.write(' gl_Position /= 0.0;')
vert.write(' return;') vert.write(' return;')
vert.write('}') vert.write('}')
if (ramp_el_len != 0):
vert.write('float n_age = clamp(p_age / p_lifetime, 0.0, 1.0);')
vert.write(f'spos.xyz *= 1 + (get_ramp_scale(n_age) - 1) * {size_over_time_factor};')
vert.write('spos.xyz *= 1 - (fhash(gl_InstanceID + 3 * pd[0][3] + pd_random) * pd_size_random);')
# vert.write('p_age /= 2;') # Match # vert.write('p_age /= 2;') # Match
# object_align_factor / 2 + gxyz # object_align_factor / 2 + gxyz
@ -57,20 +136,20 @@ def write(vert, particle_info=None, shadowmap=False):
if out_velocity: if out_velocity:
prep = '' prep = ''
vert.add_out('vec3 p_velocity') vert.add_out('vec3 p_velocity')
vert.write(prep + 'p_velocity = vec3(pd[1][0], pd[1][1], pd[1][2]);') vert.write(prep + 'p_velocity = vec3(pd[1][0] * (1 / pd_size), pd[1][1] * (1 / pd_size), pd[1][2] * (1 / pd_size));')
vert.write('p_velocity.x += fhash(gl_InstanceID) * pd[1][3] - pd[1][3] / 2;') vert.write('p_velocity.x += (fhash(gl_InstanceID + pd_random) * 2.0 / pd_size - 1.0 / pd_size) * pd[1][3];')
vert.write('p_velocity.y += fhash(gl_InstanceID + pd[0][3]) * pd[1][3] - pd[1][3] / 2;') vert.write('p_velocity.y += (fhash(gl_InstanceID + pd_random + pd[0][3]) * 2.0 / pd_size - 1.0 / pd_size) * pd[1][3];')
vert.write('p_velocity.z += fhash(gl_InstanceID + 2 * pd[0][3]) * pd[1][3] - pd[1][3] / 2;') 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];')
# factor_random = pd[1][3] # factor_random = pd[1][3]
# p.i = gl_InstanceID # p.i = gl_InstanceID
# particles.length = pd[0][3] # particles.length = pd[0][3]
# gxyz # gxyz
vert.write('p_velocity.x += (pd[2][0] * p_age) / 5;') vert.write('p_velocity.x += (pd[2][0] / (2 * pd_size)) * p_age;')
vert.write('p_velocity.y += (pd[2][1] * p_age) / 5;') vert.write('p_velocity.y += (pd[2][1] / (2 * pd_size)) * p_age;')
vert.write('p_velocity.z += (pd[2][2] * p_age) / 5;') vert.write('p_velocity.z += (pd[2][2] / (2 * pd_size)) * p_age;')
prep = 'vec3 ' prep = 'vec3 '
if out_location: if out_location:
@ -80,6 +159,96 @@ def write(vert, particle_info=None, shadowmap=False):
vert.write('spos.xyz += p_location;') vert.write('spos.xyz += p_location;')
# Rotation
if use_rotations:
if rotation_mode != 'NONE':
vert.write(f'float p_angle = ({phase_factor} + (fhash(gl_InstanceID + pd_random + 5 * pd[0][3])) * {phase_factor_random});')
vert.write('p_angle *= 3.141592;')
vert.write('float c = cos(p_angle);')
vert.write('float s = sin(p_angle);')
vert.write('vec3 center = spos.xyz - p_location;')
match rotation_mode:
case 'OB_X':
vert.write('vec3 rz = vec3(center.y, -center.x, center.z);')
vert.write('vec2 rotation = vec2(rz.y * c - rz.z * s, rz.y * s + rz.z * c);')
vert.write('spos.xyz = vec3(rz.x, rotation.x, rotation.y) + p_location;')
if (not shadowmap):
vert.write('wnormal = vec3(wnormal.y, -wnormal.x, wnormal.z);')
vert.write('vec2 n_rot = vec2(wnormal.y * c - wnormal.z * s, wnormal.y * s + wnormal.z * c);')
vert.write('wnormal = normalize(vec3(wnormal.x, n_rot.x, n_rot.y));')
case 'OB_Y':
vert.write('vec2 rotation = vec2(center.x * c + center.z * s, -center.x * s + center.z * c);')
vert.write('spos.xyz = vec3(rotation.x, center.y, rotation.y) + p_location;')
if (not shadowmap):
vert.write('wnormal = normalize(vec3(wnormal.x * c + wnormal.z * s, wnormal.y, -wnormal.x * s + wnormal.z * c));')
case 'OB_Z':
vert.write('vec3 rz = vec3(center.y, -center.x, center.z);')
vert.write('vec3 ry = vec3(-rz.z, rz.y, rz.x);')
vert.write('vec2 rotation = vec2(ry.x * c - ry.y * s, ry.x * s + ry.y * c);')
vert.write('spos.xyz = vec3(rotation.x, rotation.y, ry.z) + p_location;')
if (not shadowmap):
vert.write('wnormal = vec3(wnormal.y, -wnormal.x, wnormal.z);')
vert.write('wnormal = vec3(-wnormal.z, wnormal.y, wnormal.x);')
vert.write('vec2 n_rot = vec2(wnormal.x * c - wnormal.y * s, wnormal.x * s + wnormal.y * c);')
vert.write('wnormal = normalize(vec3(n_rot.x, n_rot.y, wnormal.z));')
case 'VEL':
vert.write('vec3 forward = -normalize(p_velocity);')
vert.write('if (length(forward) > 1e-5) {')
vert.write('vec3 world_up = vec3(0.0, 0.0, 1.0);')
vert.write('if (abs(dot(forward, world_up)) > 0.999) {')
vert.write('world_up = vec3(-1.0, 0.0, 0.0);')
vert.write('}')
vert.write('vec3 right = cross(world_up, forward);')
vert.write('if (length(right) < 1e-5) {')
vert.write('forward = -forward;')
vert.write('right = cross(world_up, forward);')
vert.write('}')
vert.write('right = normalize(right);')
vert.write('vec3 up = normalize(cross(forward, right));')
vert.write('mat3 rot = mat3(right, -forward, up);')
vert.write('mat3 phase = mat3(vec3(c, 0.0, -s), vec3(0.0, 1.0, 0.0), vec3(s, 0.0, c));')
vert.write('mat3 final_rot = rot * phase;')
vert.write('spos.xyz = final_rot * center + p_location;')
if (not shadowmap):
vert.write('wnormal = normalize(final_rot * wnormal);')
vert.write('}')
if rotation_factor_random != 0:
str_rotate_around = '''vec3 rotate_around(vec3 v, vec3 angle) {
// Rotate around X
float cx = cos(angle.x);
float sx = sin(angle.x);
v = vec3(v.x, v.y * cx - v.z * sx, v.y * sx + v.z * cx);
// Rotate around Y
float cy = cos(angle.y);
float sy = sin(angle.y);
v = vec3(v.x * cy + v.z * sy, v.y, -v.x * sy + v.z * cy);
// Rotate around Z
float cz = cos(angle.z);
float sz = sin(angle.z);
v = vec3(v.x * cz - v.y * sz, v.x * sz + v.y * cz, v.z);
return v;
}'''
vert.add_function(str_rotate_around)
vert.write(f'''vec3 r_angle = vec3((fhash(gl_InstanceID + pd_random + 6 * pd[0][3]) * 4 - 2) * {rotation_factor_random},
(fhash(gl_InstanceID + pd_random + 7 * pd[0][3]) * 4 - 2) * {rotation_factor_random},
(fhash(gl_InstanceID + pd_random + 8 * pd[0][3]) * 4 - 2) * {rotation_factor_random});''')
vert.write('vec3 r_center = spos.xyz - p_location;')
vert.write('r_center = rotate_around(r_center, r_angle);')
vert.write('spos.xyz = r_center + p_location;')
if not shadowmap:
vert.write('wnormal = normalize(rotate_around(wnormal, r_angle));')
# Particle fade # 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 == 'On' and mat_state.material.lnx_particle_fade:
vert.add_out('float p_fade') vert.add_out('float p_fade')

View File

@ -209,8 +209,7 @@ def make_instancing_and_skinning(mat: Material, mat_users: Dict[Material, List[O
global_elems.append({'name': 'ipos', 'data': 'float3'}) global_elems.append({'name': 'ipos', 'data': 'float3'})
if 'Rot' in inst: if 'Rot' in inst:
global_elems.append({'name': 'irot', 'data': 'float3'}) global_elems.append({'name': 'irot', 'data': 'float3'})
#HACK: checking `mat.arm_particle_flag` to force appending 'iscl' to the particle's vertex shader if 'Scale' in inst:
if 'Scale' in inst or mat.arm_particle_flag:
global_elems.append({'name': 'iscl', 'data': 'float3'}) global_elems.append({'name': 'iscl', 'data': 'float3'})
elif inst == 'Off': elif inst == 'Off':

View File

@ -197,6 +197,10 @@ def init_properties():
items=[('Bullet', 'Bullet', 'Bullet'), items=[('Bullet', 'Bullet', 'Bullet'),
('Oimo', 'Oimo', 'Oimo')], ('Oimo', 'Oimo', 'Oimo')],
name="Physics Engine", default='Bullet', update=assets.invalidate_compiler_cache) name="Physics Engine", default='Bullet', update=assets.invalidate_compiler_cache)
bpy.types.World.lnx_physics_fixed_step = FloatProperty(
name="Fixed Step", default=1/60, min=0, max=1,
description="Physics steps for fixed update"
)
bpy.types.World.lnx_physics_dbg_draw_wireframe = BoolProperty( bpy.types.World.lnx_physics_dbg_draw_wireframe = BoolProperty(
name="Collider Wireframes", default=False, name="Collider Wireframes", default=False,
description="Draw wireframes of the physics collider meshes and suspensions of raycast vehicle simulations" description="Draw wireframes of the physics collider meshes and suspensions of raycast vehicle simulations"
@ -357,6 +361,7 @@ def init_properties():
bpy.types.Object.lnx_rb_trigger = BoolProperty(name="Trigger", description="Disable contact response", default=False) bpy.types.Object.lnx_rb_trigger = BoolProperty(name="Trigger", description="Disable contact response", default=False)
bpy.types.Object.lnx_rb_deactivation_time = FloatProperty(name="Deactivation Time", description="Delay putting rigid body into sleep", default=0.0) bpy.types.Object.lnx_rb_deactivation_time = FloatProperty(name="Deactivation Time", description="Delay putting rigid body into sleep", default=0.0)
bpy.types.Object.lnx_rb_ccd = BoolProperty(name="Continuous Collision Detection", description="Improve collision for fast moving objects", default=False) bpy.types.Object.lnx_rb_ccd = BoolProperty(name="Continuous Collision Detection", description="Improve collision for fast moving objects", default=False)
bpy.types.Object.lnx_rb_interpolate = BoolProperty(name="Interpolation", description="Smooths out the object's transform on physics steps", default=False)
bpy.types.Object.lnx_rb_collision_filter_mask = bpy.props.BoolVectorProperty( bpy.types.Object.lnx_rb_collision_filter_mask = bpy.props.BoolVectorProperty(
name="Collision Collections Filter Mask", name="Collision Collections Filter Mask",
description="Collision collections rigid body interacts with", description="Collision collections rigid body interacts with",
@ -541,8 +546,10 @@ def init_properties():
bpy.types.Node.lnx_watch = BoolProperty(name="Watch", description="Watch value of this node in debug console", default=False) bpy.types.Node.lnx_watch = BoolProperty(name="Watch", description="Watch value of this node in debug console", default=False)
bpy.types.Node.lnx_version = IntProperty(name="Node Version", description="The version of an instanced node", default=0) bpy.types.Node.lnx_version = IntProperty(name="Node Version", description="The version of an instanced node", default=0)
# Particles # Particles
bpy.types.ParticleSettings.lnx_count_mult = FloatProperty(name="Multiply Count", description="Multiply particle count when rendering in Leenkx", default=1.0) bpy.types.ParticleSettings.lnx_auto_start = BoolProperty(name="Auto Start", description="Automatically start this particle system on load", 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_loop = BoolProperty(name="Loop", description="Loop this particle system", default=False) bpy.types.ParticleSettings.lnx_loop = BoolProperty(name="Loop", description="Loop this particle system", default=False)
bpy.types.ParticleSettings.lnx_count_mult = FloatProperty(name="Multiply Count", description="Multiply particle count when rendering in Leenkx", default=1.0)
# Actions # Actions
bpy.types.Action.lnx_root_motion_pos = BoolProperty(name="Root Motion Position", description="Enable position root motion", default=False) bpy.types.Action.lnx_root_motion_pos = BoolProperty(name="Root Motion Position", description="Enable position root motion", default=False)
bpy.types.Action.lnx_root_motion_rot = BoolProperty(name="Root Motion Rotation", description="Enable rotation root motion", default=False) bpy.types.Action.lnx_root_motion_rot = BoolProperty(name="Root Motion Rotation", description="Enable rotation root motion", default=False)

View File

@ -205,6 +205,8 @@ class LNX_PT_ParticlesPropsPanel(bpy.types.Panel):
if obj == None: if obj == None:
return return
layout.prop(obj.settings, 'lnx_auto_start')
layout.prop(obj.settings, 'lnx_is_unique')
layout.prop(obj.settings, 'lnx_loop') layout.prop(obj.settings, 'lnx_loop')
layout.prop(obj.settings, 'lnx_count_mult') layout.prop(obj.settings, 'lnx_count_mult')
@ -240,6 +242,7 @@ class LNX_PT_PhysicsPropsPanel(bpy.types.Panel):
layout.prop(obj, 'lnx_rb_angular_friction') layout.prop(obj, 'lnx_rb_angular_friction')
layout.prop(obj, 'lnx_rb_trigger') layout.prop(obj, 'lnx_rb_trigger')
layout.prop(obj, 'lnx_rb_ccd') layout.prop(obj, 'lnx_rb_ccd')
layout.prop(obj, 'lnx_rb_interpolate')
if obj.soft_body is not None: if obj.soft_body is not None:
layout.prop(obj, 'lnx_soft_body_margin') layout.prop(obj, 'lnx_soft_body_margin')
@ -2730,8 +2733,33 @@ class LeenkxUpdateListInstalledVSButton(bpy.types.Operator):
return {'FINISHED'} return {'FINISHED'}
class LNX_PT_PhysicsProps(bpy.types.Panel):
bl_label = "Leenkx Props"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_options = {'DEFAULT_CLOSED'}
bl_parent_id = "SCENE_PT_rigid_body_world"
class LNX_PT_BulletDebugDrawingPanel(bpy.types.Panel): @classmethod
def poll(cls, context):
return context.scene.rigidbody_world is not None
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
wrd = bpy.data.worlds['Lnx']
if wrd.lnx_physics_engine != 'Bullet' and wrd.lnx_physics_engine != 'Oimo':
row = layout.row()
row.alert = True
row.label(text="Physics debug drawing is only supported for the Bullet and Oimo physics engines")
col = layout.column(align=False)
col.prop(wrd, "lnx_physics_fixed_step")
class LNX_PT_PhysicsDebugDrawingPanel(bpy.types.Panel):
bl_label = "Leenkx Debug Drawing" bl_label = "Leenkx Debug Drawing"
bl_space_type = "PROPERTIES" bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW" bl_region_type = "WINDOW"
@ -2897,7 +2925,8 @@ __REG_CLASSES = (
LeenkxUpdateListAndroidEmulatorButton, LeenkxUpdateListAndroidEmulatorButton,
LeenkxUpdateListAndroidEmulatorRunButton, LeenkxUpdateListAndroidEmulatorRunButton,
LeenkxUpdateListInstalledVSButton, LeenkxUpdateListInstalledVSButton,
LNX_PT_BulletDebugDrawingPanel, LNX_PT_PhysicsProps,
LNX_PT_PhysicsDebugDrawingPanel,
LNX_OT_AddArmatureRootMotion, LNX_OT_AddArmatureRootMotion,
scene.TLM_PT_Settings, scene.TLM_PT_Settings,
scene.TLM_PT_Denoise, scene.TLM_PT_Denoise,