Merge pull request 't3du (Repe) - New Particle Nodes!' (#54) from Onek8/LNXSDK:main into main

Reviewed-on: #54
This commit is contained in:
LeenkxTeam 2025-05-28 01:54:21 +00:00
commit f450c00ff1
18 changed files with 597 additions and 38 deletions

View File

@ -775,6 +775,7 @@ class Scene {
// Attach particle systems
#if lnx_particles
if (o.particle_refs != null) {
cast(object, MeshObject).render_emitter = o.render_emitter;
for (ref in o.particle_refs) cast(object, MeshObject).setupParticleSystem(sceneName, ref);
}
#end

View File

@ -21,6 +21,7 @@ class MeshObject extends Object {
public var particleChildren: Array<MeshObject> = null;
public var particleOwner: MeshObject = null; // Particle object
public var particleIndex = -1;
public var render_emitter = true;
#end
public var cameraDistance: Float;
public var screenSize = 0.0;
@ -255,11 +256,11 @@ class MeshObject extends Object {
particleSystems[i].update(particleChildren[i], this);
}
}
if (particleSystems != null && particleSystems.length > 0 && !raw.render_emitter) return;
if (particleSystems != null && particleSystems.length > 0 && !render_emitter) return;
if (particleSystems == null && cullMaterial(context)) return;
#else
if (cullMaterial(context)) return;
#end
if (cullMaterial(context)) return;
// Get lod
var mats = materials;
var lod = this;

View File

@ -18,29 +18,29 @@ class ParticleSystem {
public var speed = 1.0;
var particles: Array<Particle>;
var ready: Bool;
var frameRate = 24;
var lifetime = 0.0;
var animtime = 0.0;
var time = 0.0;
var spawnRate = 0.0;
public var frameRate = 24;
public var lifetime = 0.0;
public var animtime = 0.0;
public var time = 0.0;
public var spawnRate = 0.0;
var seed = 0;
var r: TParticleData;
var gx: Float;
var gy: Float;
var gz: Float;
var alignx: Float;
var aligny: Float;
var alignz: Float;
public var r: TParticleData;
public var gx: Float;
public var gy: Float;
public var gz: Float;
public var alignx: Float;
public var aligny: Float;
public var alignz: Float;
var dimx: Float;
var dimy: Float;
var tilesx: Int;
var tilesy: Int;
var tilesFramerate: Int;
var count = 0;
var lap = 0;
var lapTime = 0.0;
public var count = 0;
public var lap = 0;
public var lapTime = 0.0;
var m = Mat4.identity();
var ownerLoc = new Vec4();
@ -149,7 +149,7 @@ class ParticleSystem {
// GPU particles transform is attached to owner object
}
function setupGeomGpu(object: MeshObject, owner: MeshObject) {
public function setupGeomGpu(object: MeshObject, owner: MeshObject) {
var instancedData = new Float32Array(particles.length * 6);
var i = 0;

View File

@ -0,0 +1,99 @@
package leenkx.logicnode;
import iron.data.SceneFormat.TSceneFormat;
import iron.data.Data;
import iron.object.Object;
class AddParticleToObjectNode extends LogicNode {
public var property0: String;
public function new(tree: LogicTree) {
super(tree);
}
override function run(from: Int) {
#if lnx_particles
if (property0 == 'Scene Active'){
var objFrom: Object = inputs[1].get();
var slot: Int = inputs[2].get();
var objTo: Object = inputs[3].get();
if (objFrom == null || objTo == null) return;
var mobjFrom = cast(objFrom, iron.object.MeshObject);
var psys = mobjFrom.particleSystems != null ? mobjFrom.particleSystems[slot] :
mobjFrom.particleOwner != null && mobjFrom.particleOwner.particleSystems != null ? mobjFrom.particleOwner.particleSystems[slot] : null;
if (psys == null) return;
var mobjTo = cast(objTo, iron.object.MeshObject);
mobjTo.setupParticleSystem(iron.Scene.active.raw.name, {name: 'LnxPS', seed: 0, particle: psys.r.name});
mobjTo.render_emitter = inputs[4].get();
iron.Scene.active.spawnObject(psys.data.raw.instance_object, null, function(o: Object) {
if (o != null) {
var c: iron.object.MeshObject = cast o;
if (mobjTo.particleChildren == null) mobjTo.particleChildren = [];
mobjTo.particleChildren.push(c);
c.particleOwner = mobjTo;
c.particleIndex = mobjTo.particleChildren.length - 1;
}
});
var oslot: Int = mobjTo.particleSystems.length-1;
var opsys = mobjTo.particleSystems[oslot];
opsys.setupGeomGpu(mobjTo.particleChildren[oslot], mobjTo);
} else {
var sceneName: String = inputs[1].get();
var objectName: String = inputs[2].get();
var slot: Int = inputs[3].get();
var mobjTo: Object = inputs[4].get();
var mobjTo = cast(mobjTo, iron.object.MeshObject);
#if lnx_json
sceneName += ".json";
#elseif lnx_compress
sceneName += ".lz4";
#end
Data.getSceneRaw(sceneName, (rawScene: TSceneFormat) -> {
for (obj in rawScene.objects) {
if (obj.name == objectName) {
mobjTo.setupParticleSystem(sceneName, obj.particle_refs[slot]);
mobjTo.render_emitter = inputs[5].get();
iron.Scene.active.spawnObject(rawScene.particle_datas[slot].instance_object, null, function(o: Object) {
if (o != null) {
var c: iron.object.MeshObject = cast o;
if (mobjTo.particleChildren == null) mobjTo.particleChildren = [];
mobjTo.particleChildren.push(c);
c.particleOwner = mobjTo;
c.particleIndex = mobjTo.particleChildren.length - 1;
}
}, true, rawScene);
var oslot: Int = mobjTo.particleSystems.length-1;
var opsys = mobjTo.particleSystems[oslot];
opsys.setupGeomGpu(mobjTo.particleChildren[oslot], mobjTo);
break;
}
}
});
}
#end
runOutput(0);
}
}

View File

@ -0,0 +1,66 @@
package leenkx.logicnode;
import iron.object.Object;
class GetParticleDataNode extends LogicNode {
public function new(tree: LogicTree) {
super(tree);
}
override function get(from: Int): Dynamic {
var object: Object = inputs[0].get();
var slot: Int = inputs[1].get();
if (object == null) return null;
#if lnx_particles
var mo = cast(object, iron.object.MeshObject);
var psys = mo.particleSystems != null ? mo.particleSystems[slot] :
mo.particleOwner != null && mo.particleOwner.particleSystems != null ? mo.particleOwner.particleSystems[slot] : null;
if (psys == null) return null;
return switch (from) {
case 0:
psys.r.name;
case 1:
psys.r.particle_size;
case 2:
psys.r.frame_start;
case 3:
psys.r.frame_end;
case 4:
psys.lifetime;
case 5:
psys.r.lifetime;
case 6:
psys.r.emit_from;
case 7:
new iron.math.Vec3(psys.alignx*2, psys.aligny*2, psys.alignz*2);
case 8:
psys.r.factor_random;
case 9:
new iron.math.Vec3(psys.gx, psys.gy, psys.gz);
case 10:
psys.r.weight_gravity;
case 11:
psys.speed;
case 12:
psys.time;
case 13:
psys.lap;
case 14:
psys.lapTime;
case 15:
psys.count;
default:
null;
}
#end
return null;
}
}

View File

@ -0,0 +1,38 @@
package leenkx.logicnode;
import iron.object.Object;
class GetParticleNode extends LogicNode {
public function new(tree: LogicTree) {
super(tree);
}
override function get(from: Int): Dynamic {
var object: Object = inputs[0].get();
if (object == null) return null;
#if lnx_particles
var mo = cast(object, iron.object.MeshObject);
switch (from) {
case 0:
var names: Array<String> = [];
if (mo.particleSystems != null)
for (psys in mo.particleSystems)
names.push(psys.r.name);
return names;
case 1:
return mo.particleSystems != null ? mo.particleSystems.length : 0;
case 2:
return mo.render_emitter;
default:
null;
}
#end
return null;
}
}

View File

@ -0,0 +1,64 @@
package leenkx.logicnode;
import iron.object.Object;
class RemoveParticleFromObjectNode extends LogicNode {
public var property0: String;
public function new(tree: LogicTree) {
super(tree);
}
override function run(from: Int) {
#if lnx_particles
var object: Object = inputs[1].get();
if (object == null) return;
var mo = cast(object, iron.object.MeshObject);
if (mo.particleSystems == null) return;
if (property0 == 'All'){
mo.particleSystems = null;
for (c in mo.particleChildren) c.remove();
mo.particleChildren = null;
mo.particleOwner = null;
mo.render_emitter = true;
}
else {
var slot: Int = -1;
if (property0 == 'Name'){
var name: String = inputs[2].get();
for (i => psys in mo.particleSystems){
if (psys.r.name == name){ slot = i; break; }
}
}
else slot = inputs[2].get();
if (mo.particleSystems.length > slot){
for (i in slot+1...mo.particleSystems.length){
var mi = cast(mo.particleChildren[i], iron.object.MeshObject);
mi.particleIndex = mi.particleIndex - 1;
}
mo.particleSystems.splice(slot, 1);
mo.particleChildren[slot].remove();
mo.particleChildren.splice(slot, 1);
}
if (slot == 0){
mo.particleSystems = null;
mo.particleChildren = null;
mo.particleOwner = null;
mo.render_emitter = true;
}
}
#end
runOutput(0);
}
}

View File

@ -0,0 +1,75 @@
package leenkx.logicnode;
import iron.object.Object;
class SetParticleDataNode extends LogicNode {
public var property0: String;
public function new(tree: LogicTree) {
super(tree);
}
override function run(from: Int) {
#if lnx_particles
var object: Object = inputs[1].get();
var slot: Int = inputs[2].get();
if (object == null) return;
var mo = cast(object, iron.object.MeshObject);
var psys = mo.particleSystems != null ? mo.particleSystems[slot] :
mo.particleOwner != null && mo.particleOwner.particleSystems != null ? mo.particleOwner.particleSystems[slot] : null; if (psys == null) return;
switch (property0) {
case 'Particle Size':
psys.r.particle_size = inputs[3].get();
case 'Frame Start':
psys.r.frame_start = inputs[3].get();
psys.animtime = (psys.r.frame_end - psys.r.frame_start) / psys.frameRate;
psys.spawnRate = ((psys.r.frame_end - psys.r.frame_start) / psys.count) / psys.frameRate;
case 'Frame End':
psys.r.frame_end = inputs[3].get();
psys.animtime = (psys.r.frame_end - psys.r.frame_start) / psys.frameRate;
psys.spawnRate = ((psys.r.frame_end - psys.r.frame_start) / psys.count) / psys.frameRate;
case 'Lifetime':
psys.lifetime = inputs[3].get() / psys.frameRate;
case 'Lifetime Random':
psys.r.lifetime_random = inputs[3].get();
case 'Emit From':
var emit_from: Int = inputs[3].get();
if (emit_from == 0 || emit_from == 1 || emit_from == 2) {
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);
}
case 'Velocity':
var vel: iron.math.Vec3 = inputs[3].get();
psys.alignx = vel.x / 2;
psys.aligny = vel.y / 2;
psys.alignz = vel.z / 2;
case 'Velocity Random':
psys.r.factor_random = inputs[3].get();
case 'Weight Gravity':
psys.r.weight_gravity = inputs[3].get();
if (iron.Scene.active.raw.gravity != null) {
psys.gx = iron.Scene.active.raw.gravity[0] * psys.r.weight_gravity;
psys.gy = iron.Scene.active.raw.gravity[1] * psys.r.weight_gravity;
psys.gz = iron.Scene.active.raw.gravity[2] * psys.r.weight_gravity;
}
else {
psys.gx = 0;
psys.gy = 0;
psys.gz = -9.81 * psys.r.weight_gravity;
}
case 'Speed':
psys.speed = inputs[3].get();
default:
null;
}
#end
runOutput(0);
}
}

View File

@ -0,0 +1,23 @@
package leenkx.logicnode;
import iron.object.Object;
class SetParticleRenderEmitterNode extends LogicNode {
public function new(tree: LogicTree) {
super(tree);
}
override function run(from: Int) {
#if lnx_particles
var object: Object = inputs[1].get();
if (object == null) return;
cast(object, iron.object.MeshObject).render_emitter = inputs[2].get();
#end
runOutput(0);
}
}

View File

@ -11,13 +11,16 @@ class SetParticleSpeedNode extends LogicNode {
override function run(from: Int) {
#if lnx_particles
var object: Object = inputs[1].get();
var speed: Float = inputs[2].get();
var slot: Int = inputs[2].get();
var speed: Float = inputs[3].get();
if (object == null) return;
var mo = cast(object, iron.object.MeshObject);
var psys = mo.particleSystems.length > 0 ? mo.particleSystems[0] : null;
if (psys == null) mo.particleOwner.particleSystems[0];
var psys = mo.particleSystems != null ? mo.particleSystems[slot] :
mo.particleOwner != null && mo.particleOwner.particleSystems != null ? mo.particleOwner.particleSystems[slot] : null;
if (psys == null) return;
psys.speed = speed;

View File

@ -47,7 +47,8 @@ def init_categories():
lnx_nodes.add_category('Navmesh', icon='UV_VERTEXSEL', section="motion")
lnx_nodes.add_category('Transform', icon='TRANSFORM_ORIGINS', section="motion")
lnx_nodes.add_category('Physics', icon='PHYSICS', section="motion")
lnx_nodes.add_category('Particle', icon='PARTICLE_DATA', section="motion")
lnx_nodes.add_category('Array', icon='MOD_ARRAY', section="values")
lnx_nodes.add_category('Map', icon='SHORTDISPLAY', section="values")
lnx_nodes.add_category('Database', icon='MESH_CYLINDER', section="values")

View File

@ -0,0 +1,41 @@
from lnx.logicnode.lnx_nodes import *
class AddParticleToObjectNode(LnxLogicTreeNode):
"""Sets the speed of the given particle source."""
bl_idname = 'LNAddParticleToObjectNode'
bl_label = 'Add Particle To Object'
lnx_version = 1
def remove_extra_inputs(self, context):
while len(self.inputs) > 1:
self.inputs.remove(self.inputs[-1])
if self.property0 == 'Scene':
self.add_input('LnxStringSocket', 'Scene From Name')
self.add_input('LnxStringSocket', 'Object From Name')
else:
self.add_input('LnxNodeSocketObject', 'Object From')
self.add_input('LnxIntSocket', 'Slot')
self.add_input('LnxNodeSocketObject', 'Object To')
self.add_input('LnxBoolSocket', 'Render Emitter', default_value = True)
property0: HaxeEnumProperty(
'property0',
items = [('Scene Active', 'Scene Active', 'Scene Active'),
('Scene', 'Scene', 'Scene')],
name='', default='Scene Active', update=remove_extra_inputs)
def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxNodeSocketObject', 'Object From')
self.add_input('LnxIntSocket', 'Slot')
self.add_input('LnxNodeSocketObject', 'Object To')
self.add_input('LnxBoolSocket', 'Render Emitter', default_value = True)
self.add_output('LnxNodeSocketAction', 'Out')
def draw_buttons(self, context, layout):
layout.prop(self, 'property0')

View File

@ -0,0 +1,14 @@
from lnx.logicnode.lnx_nodes import *
class GetParticleNode(LnxLogicTreeNode):
"""Returns the Particle Systems of an object."""
bl_idname = 'LNGetParticleNode'
bl_label = 'Get Particle'
lnx_version = 1
def lnx_init(self, context):
self.inputs.new('LnxNodeSocketObject', 'Object')
self.outputs.new('LnxNodeSocketArray', 'Names')
self.outputs.new('LnxIntSocket', 'Length')
self.outputs.new('LnxBoolSocket', 'Render Emitter')

View File

@ -0,0 +1,31 @@
from lnx.logicnode.lnx_nodes import *
class GetParticleDataNode(LnxLogicTreeNode):
"""Returns the data of the given Particle System."""
bl_idname = 'LNGetParticleDataNode'
bl_label = 'Get Particle Data'
lnx_version = 1
def lnx_init(self, context):
self.inputs.new('LnxNodeSocketObject', 'Object')
self.inputs.new('LnxIntSocket', 'Slot')
self.outputs.new('LnxStringSocket', 'Name')
self.outputs.new('LnxFloatSocket', 'Particle Size')
self.outputs.new('LnxIntSocket', 'Frame Start')
self.outputs.new('LnxIntSocket', 'Frame End')
self.outputs.new('LnxIntSocket', 'Lifetime')
self.outputs.new('LnxFloatSocket', 'Lifetime Random')
self.outputs.new('LnxIntSocket', 'Emit From')
self.outputs.new('LnxVectorSocket', 'Velocity')
self.outputs.new('LnxFloatSocket', 'Velocity Random')
self.outputs.new('LnxVectorSocket', 'Gravity')
self.outputs.new('LnxFloatSocket', 'Weight Gravity')
self.outputs.new('LnxFloatSocket', 'Speed')
self.outputs.new('LnxFloatSocket', 'Time')
self.outputs.new('LnxFloatSocket', 'Lap')
self.outputs.new('LnxFloatSocket', 'Lap Time')
self.outputs.new('LnxIntSocket', 'Count')

View File

@ -0,0 +1,33 @@
from lnx.logicnode.lnx_nodes import *
class RemoveParticleFromObjectNode(LnxLogicTreeNode):
"""Remove Particle From Object."""
bl_idname = 'LNRemoveParticleFromObjectNode'
bl_label = 'Remove Particle From Object'
lnx_version = 1
def remove_extra_inputs(self, context):
while len(self.inputs) > 2:
self.inputs.remove(self.inputs[-1])
if self.property0 == 'Slot':
self.add_input('LnxIntSocket', 'Slot')
if self.property0 == 'Name':
self.add_input('LnxStringSocket', 'Name')
property0: HaxeEnumProperty(
'property0',
items = [('Slot', 'Slot', 'Slot'),
('Name', 'Name', 'Name'),
('All', 'All', 'All')],
name='', default='Slot', update=remove_extra_inputs)
def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxNodeSocketObject', 'Object')
self.add_input('LnxIntSocket', 'Slot')
self.add_output('LnxNodeSocketAction', 'Out')
def draw_buttons(self, context, layout):
layout.prop(self, 'property0')

View File

@ -0,0 +1,58 @@
from lnx.logicnode.lnx_nodes import *
class SetParticleDataNode(LnxLogicTreeNode):
"""Sets the parameters of the given particle system."""
bl_idname = 'LNSetParticleDataNode'
bl_label = 'Set Particle Data'
lnx_version = 1
def remove_extra_inputs(self, context):
while len(self.inputs) > 3:
self.inputs.remove(self.inputs[-1])
if self.property0 == 'Particle Size':
self.add_input('LnxFloatSocket', 'Particle Size')
if self.property0 == 'Frame End':
self.add_input('LnxIntSocket', 'Frame End')
if self.property0 == 'Frame Start':
self.add_input('LnxIntSocket', 'Frame Start')
if self.property0 == 'Lifetime':
self.add_input('LnxIntSocket', 'Lifetime')
if self.property0 == 'Lifetime Random':
self.add_input('LnxFloatSocket', 'Lifetime Random')
if self.property0 == 'Emit From':
self.add_input('LnxIntSocket', 'Emit From')
if self.property0 == 'Velocity':
self.add_input('LnxVectorSocket', 'Velocity')
if self.property0 == 'Velocity Random':
self.add_input('LnxFloatSocket', 'Velocity Random')
if self.property0 == 'Weight Gravity':
self.add_input('LnxFloatSocket', 'Weight Gravity')
if self.property0 == 'Speed':
self.add_input('LnxFloatSocket', 'Speed')
property0: HaxeEnumProperty(
'property0',
items = [('Particle Size', 'Particle Size', 'for the system'),
('Frame Start', 'Frame Start', 'for the system'),
('Frame End', 'Frame End', 'for the system'),
('Lifetime', 'Lifetime', 'for the instance'),
('Lifetime Random', 'Lifetime Random', 'for the system'),
('Emit From', 'Emit From', 'for the system (Vertices:0 Faces:1 Volume: 2)'),
('Velocity', 'Velocity', 'for the instance'),
('Velocity Random', 'Velocity Random', 'for the system'),
('Weight Gravity', 'Weight Gravity', 'for the instance'),
('Speed', 'Speed', 'for the instance')],
name='', default='Speed', update=remove_extra_inputs)
def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxNodeSocketObject', 'Object')
self.add_input('LnxIntSocket', 'Slot')
self.add_input('LnxFloatSocket', 'Speed', default_value=1.0)
self.add_output('LnxNodeSocketAction', 'Out')
def draw_buttons(self, context, layout):
layout.prop(self, 'property0')

View File

@ -1,14 +1,22 @@
from lnx.logicnode.lnx_nodes import *
class SetParticleSpeedNode(LnxLogicTreeNode):
"""Sets the speed of the given particle source."""
bl_idname = 'LNSetParticleSpeedNode'
bl_label = 'Set Particle Speed'
lnx_version = 1
def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxNodeSocketObject', 'Object')
self.add_input('LnxFloatSocket', 'Speed', default_value=1.0)
self.add_output('LnxNodeSocketAction', 'Out')
from lnx.logicnode.lnx_nodes import *
class SetParticleSpeedNode(LnxLogicTreeNode):
"""Sets the speed of the given particle source."""
bl_idname = 'LNSetParticleSpeedNode'
bl_label = 'Set Particle Speed'
lnx_version = 2
def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxNodeSocketObject', 'Object')
self.add_input('LnxIntSocket', 'Slot')
self.add_input('LnxFloatSocket', 'Speed', default_value=1.0)
self.add_output('LnxNodeSocketAction', 'Out')
def get_replacement_node(self, node_tree: bpy.types.NodeTree):
if self.lnx_version not in (0, 1):
raise LookupError()
return NodeReplacement.Identity(self)

View File

@ -0,0 +1,3 @@
from lnx.logicnode.lnx_nodes import add_node_section
add_node_section(name='default', category='Particle')