// particle system // // last one we need to implement is a ParticleComposite, // this is a component which owns a particle emitter component (adding one if it does not exist) // // and generates/modifies particles system properties over time. // // just thinking... with meta reflection. I wonder if it is possible to codegen // a generic "timeline" runner capable of modifying any arbitrary float, bool etc... over time. // // that could be pub const EmitterState = enum { dead, // emitter is not creating any particles alive, // emitter is creating particles up to max_porticles paused, // emitter is not updating }; pub const ParticleRandRangef = struct { min: f32 = 0, max: f32 = 0, pub var randomFunc: std.Random = undefined; pub var randomEngine: std.Random.DefaultPrng = undefined; pub fn set(self: *@This(), v: f32) void { self.min = v; self.max = v; } pub fn getRange(self: @This()) f32 { if (std.math.approxEqAbs(f32, self.min, self.max, 0.0001)) return self.min; const x = (randomFunc.float(f32) + randomFunc.float(f32) + randomFunc.float(f32) + randomFunc.float(f32)) / 4; return x * (self.max - self.min) + self.min; } }; pub const EmitterShape = union(enum(u8)) { spherical: struct { radius: f32 = 50.0, innerRadius: f32 = 0, // distrobution: = }, conal: struct { direction: core.Vectorf, angle: f32, innerRadius: f32 = 0, }, }; pub const Life = struct { current: f32, max: f32, }; pub const ParticlePVA = struct { position: core.f32x4 = core.f32x4_zero, velocity: core.f32x4 = core.f32x4_zero, acceleration: core.f32x4 = core.f32x4_zero, }; pub const Rotationals = struct { quat: core.f32x4 = .{}, spinVector: core.f32x4 = .{}, angularMomentum: f32 = 0.0, }; pub const RenderInfo = struct { meshIndex: u32 = 0, textureIndex: u32 = 0, var quad_mesh: ?rend.IndexedMesh = null; var default_texture: ?*rend.Texture = null; pub fn getMesh(self: @This(), emitter: *const ParticleEmitter) rend.IndexedMesh { if (emitter.mesh.items.len == 0) { if (quad_mesh == null) { quad_mesh = rend.getMesh("m_screenPlane").?; } return quad_mesh.?; } return emitter.mesh.items[self.meshIndex]; } pub fn getTexture(self: @This(), emitter: *const ParticleEmitter) *rend.Texture { if (emitter.texture.items.len == 0) { if (default_texture == null) { default_texture = rend.getTexture("t_white").?; } return default_texture.?; } return emitter.texture.items[self.textureIndex]; } }; pub const ParticleTimelineBurst = struct { count: u32, }; pub const ParticleTimeline = struct { func: ParticleTimelineFunc = .{} }; pub const ParticleTimelineFunc = union(enum(u8)) { burst: ParticleTimelineBurst }; pub const VfxEmitter = struct { emitter: ParticleEmitter, timeline: ParticleTimeline, parent: u32, transform: core.Mat, }; pub const VfxComponent = struct { // aims to replace the ParticleEmitter // instead of having just one particle emitter, the VfxComponent manages scenes for multiple particle emitters pub var BaseContainer: *core.SparseSet(VfxComponent) = undefined; pub const ComponentName = "rend.VfxComponent"; pub const ScriptExports: []const []const u8 = &.{}; entity: core.Entity = undefined, emitterList: std.MultiArrayList(VfxEmitter) = .{}, pub fn initECS(self: *@This(), handle: core.SetHandle) void { self.entity = core.Entity{ .handle = handle }; } pub fn addEmitter(self: *@This()) *ParticleEmitter { _ = self; } pub fn update(self: *@This(), dt: f64) void { _ = dt; _ = self; } pub fn deinitECS(self: *@This(), handle: core.SetHandle) void { for (self.emitters.items) |emitter| { emitter.deinitECS(handle); } } }; pub const ParticleEmitter = struct { pub var BaseContainer: *core.SparseSet(ParticleEmitter) = undefined; pub const ComponentName = "ParticleEmitter"; pub const ScriptExports: []const []const u8 = &.{}; // per particle information life: std.ArrayListUnmanaged(Life) = .{}, particlesPVA: std.ArrayListUnmanaged(ParticlePVA) = .{}, finals: std.ArrayListUnmanaged(core.Mat) = .{}, size: std.ArrayListUnmanaged(f32) = .{}, particleAcceleration: ParticleRandRangef = .{ .min = 0.0, .max = 0.0 }, particleVelocity: ParticleRandRangef = .{ .min = 8, .max = 10 }, particleLife: ParticleRandRangef = .{ .min = 2, .max = 2 }, particleSizeSpawn: ParticleRandRangef = .{ .min = 0.8, .max = 1.0 }, spawnRate: ParticleRandRangef = .{ .min = 10, .max = 10 }, nextSpawn: f64 = 0.0, gravity: ?core.Vectorf = null, maxParticles: u32 = 100, // globalScale: f32 = 1.0, not implemented state: EmitterState = .dead, billboard: bool = true, // if billboard is set, the billboard bit will be set // and only position and scale will be used to render the particle. // an empty list means that this will use quad_mesh mesh: rend.IndexedMesh = undefined, // empty list means will use t_white texture: *rend.Texture = undefined, emitterLife: f64 = 0.0, // 0.0 means this emitter lives forever. emitterMaxLife: f64 = 0.0, emitterShape: EmitterShape = .{ .spherical = .{} }, useLocalScene: bool = false, _showDebug: bool = false, entity: core.Entity = undefined, pub fn updatePVA(self: *@This(), dt: f64) void { // should generate SIMD operations const gravity = if (self.gravity) |gravity| gravity.toZm() else core.f32x4_zero; for (self.particlesPVA.items) |*pva| { pva.velocity = pva.acceleration * @as(core.f32x4, @splat(@floatCast(dt))) + gravity * @as(core.f32x4, @splat(@floatCast(dt))) + pva.velocity; pva.position = pva.velocity * @as(core.f32x4, @splat(@floatCast(dt))) + pva.position; if (self._showDebug) { core.debugSphere(core.Vectorf.fromArray(pva.position), 0.1, .{ .color = .{ .y = 1.0, .x = positionLength(pva.velocity) / 10 } }); // core.debugSphere(core.Vectorf.Zeroes, 0.1, .{}); } } } inline fn positionLength(f: core.f32x4) f32 { return @sqrt((f[0] * f[0]) + (f[1] * f[1]) + (f[2] * f[2]) + (f[3] * f[3])); } pub fn updateParticleLife(self: *@This(), dt: f64) void { // should generate SIMD operations const dt32: f32 = @floatCast(dt); for (self.life.items) |*life| { if (life.max > 0) life.current -= dt32; } var i: usize = 0; while (i < self.life.items.len) : (i += 1) { if (self.life.items[i].current < 0) { self.removeParticle(i); } } if (self.useLocalScene) { switch (self.emitterShape) { .spherical => |spherical| { i = 0; while (i < self.particlesPVA.items.len) : (i += 1) { if (positionLength(self.particlesPVA.items[i].position) > spherical.radius) { self.removeParticle(i); } } }, .conal => { @panic("not implemented"); }, } } } pub fn removeParticle(self: *@This(), i: usize) void { _ = self.life.swapRemove(i); _ = self.particlesPVA.swapRemove(i); _ = self.finals.swapRemove(i); _ = self.size.swapRemove(i); } pub fn updateSpawn(self: *@This(), dt: f64) void { self.nextSpawn -= dt; if (self.nextSpawn > 0) { return; } if (self.nextSpawn < 0 and self.life.items.len >= self.maxParticles) { self.nextSpawn = 0.0; return; } while (self.nextSpawn < 0) { self.spawnParticle() catch { core.engine_err(" UNABLE TO SPAWN PARTICLE", .{}); }; // generate random values for the next spawn self.nextSpawn += 1.0 / @as(f64, @floatCast(self.spawnRate.getRange())); } } pub fn start(self: *@This()) void { self.state = .alive; } pub fn generatePVA(self: *@This()) ParticlePVA { switch (self.emitterShape) { .spherical => |spherical| { const radius = ParticleRandRangef{ .min = -spherical.innerRadius, .max = spherical.innerRadius }; const oneRand = ParticleRandRangef{ .min = -1.0, .max = 1.0 }; const v = core.Vectorf{ .x = oneRand.getRange(), .y = oneRand.getRange(), .z = oneRand.getRange() }; const v2 = v.normalize(); var pva: ParticlePVA = .{ .velocity = v2.fmul(self.particleVelocity.getRange()).toZm(), .position = .{ radius.getRange(), radius.getRange(), radius.getRange(), 1.0, }, .acceleration = v2.fmul(self.particleAcceleration.getRange()).toZm(), }; if (!self.useLocalScene) { const scenePos = core.zm.mul(pva.position, self.entity.fetch(core.Scene).?.getPosRot().toTransform()); pva.position = scenePos; } return pva; }, .conal => |conal| { _ = conal; @panic("not implemented"); }, } } fn spawnParticle(self: *@This()) !void { const pva = self.generatePVA(); const life = self.particleLife.getRange(); try self.life.append(particleAllocator(), .{ .current = life, .max = life }); try self.particlesPVA.append(particleAllocator(), pva); try self.finals.append(particleAllocator(), std.mem.zeroes(core.Mat)); try self.size.append(particleAllocator(), self.particleSizeSpawn.getRange()); } fn updateLife(self: *@This(), dt: f64) void { if (self.emitterMaxLife <= 0) { return; } self.emitterLife -= dt; if (self.emitterLife <= 0) { self.state = .dead; } } pub fn update(self: *@This(), dt: f64) void { if (self.state == .paused) { return; } if (self.state != .dead) { self.updateSpawn(dt); } // core.debugSphere(self.entity.fetch(core.Scene).?.getPosition(), self.emitterShape.spherical.radius, .{}); self.updatePVA(dt); self.updateParticleLife(dt); self.updateLife(dt); self.updateFinals(); } pub fn updateFinals(self: *@This()) void { if (self.useLocalScene) { const scene = self.entity.fetch(core.Scene).?; const transform = scene.getPosRot().toTransform(); for (self.particlesPVA.items, 0..) |pva, i| { const p = core.zm.translationV(pva.position); self.finals.items[i] = core.zm.mul(p, transform); } } else { for (self.particlesPVA.items, 0..) |pva, i| { const p = core.zm.translationV(pva.position); self.finals.items[i] = p; } } } var t_whiteName: core.Name = core.DefineName("t_white"); var m_quad: core.Name = core.DefineName("m_screenPlane"); pub fn initECS(self: *@This(), handle: core.ObjectHandle) void { // get the mesh component self.entity = core.Entity{ .handle = handle }; self.texture = rend.getTexture(&t_whiteName).?; core.engine_logs("wtf"); self.mesh = rend.getMeshByName(&m_quad).?; if (self.entity.fetch(core.Scene)) |scene| { _ = scene; } else { _ = self.entity.addComponent(core.Scene); } } pub fn deinitECS(self: *@This(), handle: core.ObjectHandle) void { _ = handle; self.deinit(); } pub fn deinit(self: *@This()) void { self.life.deinit(particleAllocator()); self.particlesPVA.deinit(particleAllocator()); self.finals.deinit(particleAllocator()); self.size.deinit(particleAllocator()); } }; fn particleAllocator() std.mem.Allocator { return core.get(ParticleSystem).particleArena.allocator(); } // var gParticleAllocator: std.mem.Allocator = undefined; pub const ParticleSystem = struct { particleArena: std.heap.ArenaAllocator, allocator: std.mem.Allocator, pub var NeonObjectTable: core.EngineObjectVTable = core.EngineObjectVTable.from(@This(), "rend.ParticleSystem"); pub fn create(allocator: std.mem.Allocator) !*@This() { const self = try allocator.create(@This()); self.* = .{ .particleArena = std.heap.ArenaAllocator.init(allocator), .allocator = allocator, }; ParticleRandRangef.randomEngine = std.Random.DefaultPrng.init(0x1234); ParticleRandRangef.randomFunc = ParticleRandRangef.randomEngine.random(); return self; } pub fn tick(self: *@This(), dt: f64) void { var z = tracy.ZoneN(@src(), "Particle System"); defer z.End(); _ = self; for (ParticleEmitter.BaseContainer.dense.items) |*emitter| { emitter.value.update(dt); } } pub fn destroy(self: *@This()) void { self.particleArena.deinit(); self.allocator.destroy(self); } }; const core = @import("core"); const rend = @import("../rend.zig"); const std = @import("std"); const tracy = core.tracy;